MDL-57972 javascript: add upgrade instructions for truncate.js
[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 */
35be5826 26define(['core/mustache',
9bdcf579
DW
27 'jquery',
28 'core/ajax',
29 'core/str',
30 'core/notification',
31 'core/url',
39bf2a98 32 'core/log',
4b9e5326 33 'core/config',
28de7771 34 'core/localstorage',
1fca8a7b 35 'core/event',
29879f8f
FM
36 'core/yui',
37 'core/log'
9bdcf579 38 ],
39bf2a98 39 function(mustache, $, ajax, str, notification, coreurl, log, config, storage, event, Y, Log) {
9bdcf579 40
8d00afb1
DW
41 // Module variables.
42 /** @var {Number} uniqInstances Count of times this constructor has been called. */
43 var uniqInstances = 0;
9bdcf579 44
39bf2a98 45 /** @var {String[]} templateCache - Cache of already loaded template strings */
9bdcf579
DW
46 var templateCache = {};
47
39bf2a98
DW
48 /** @var {Promise[]} templatePromises - Cache of already loaded template promises */
49 var templatePromises = {};
f20a336b 50
8d00afb1
DW
51 /**
52 * Constructor
53 *
54 * Each call to templates.render gets it's own instance of this class.
55 */
56 var Renderer = function() {
57 this.requiredStrings = [];
58 this.requiredJS = [];
59 this.currentThemeName = '';
60 };
61 // Class variables and functions.
62
9bdcf579 63 /** @var {string[]} requiredStrings - Collection of strings found during the rendering of one template */
8d00afb1 64 Renderer.prototype.requiredStrings = null;
9bdcf579
DW
65
66 /** @var {string[]} requiredJS - Collection of js blocks found during the rendering of one template */
8d00afb1 67 Renderer.prototype.requiredJS = null;
9bdcf579
DW
68
69 /** @var {String} themeName for the current render */
8d00afb1 70 Renderer.prototype.currentThemeName = '';
9bdcf579 71
f992dcf6
DP
72 /**
73 * Load a template from the cache or local storage or ajax request.
74 *
75 * @method getTemplate
76 * @private
77 * @param {string} templateName - should consist of the component and the name of the template like this:
78 * core/menu (lib/templates/menu.mustache) or
79 * tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache)
80 * @return {Promise} JQuery promise object resolved when the template has been fetched.
81 */
39bf2a98 82 Renderer.prototype.getTemplate = function(templateName) {
f992dcf6
DP
83 var parts = templateName.split('/');
84 var component = parts.shift();
85 var name = parts.shift();
86
8d00afb1 87 var searchKey = this.currentThemeName + '/' + templateName;
f992dcf6
DP
88
89 // First try request variables.
39bf2a98
DW
90 if (searchKey in templatePromises) {
91 return templatePromises[searchKey];
f992dcf6
DP
92 }
93
94 // Now try local storage.
95 var cached = storage.get('core_template/' + searchKey);
96
97 if (cached) {
39bf2a98
DW
98 templateCache[searchKey] = cached;
99 templatePromises[searchKey] = $.Deferred().resolve(cached).promise();
100 return templatePromises[searchKey];
f992dcf6
DP
101 }
102
103 // Oh well - load via ajax.
104 var promises = ajax.call([{
105 methodname: 'core_output_load_template',
35be5826 106 args: {
f992dcf6
DP
107 component: component,
108 template: name,
8d00afb1 109 themename: this.currentThemeName
f992dcf6 110 }
39bf2a98 111 }], true, false);
f992dcf6 112
39bf2a98 113 templatePromises[searchKey] = promises[0].then(
35be5826 114 function(templateSource) {
39bf2a98 115 templateCache[searchKey] = templateSource;
f992dcf6 116 storage.set('core_template/' + searchKey, templateSource);
f20a336b 117 return templateSource;
f992dcf6
DP
118 }
119 );
39bf2a98 120 return templatePromises[searchKey];
f992dcf6
DP
121 };
122
123 /**
124 * Load a partial from the cache or ajax.
125 *
126 * @method partialHelper
127 * @private
128 * @param {string} name The partial name to load.
129 * @return {string}
130 */
8d00afb1 131 Renderer.prototype.partialHelper = function(name) {
f992dcf6 132
39bf2a98
DW
133 var searchKey = this.currentThemeName + '/' + name;
134
135 if (!(searchKey in templateCache)) {
136 notification.exception(new Error('Failed to pre-fetch the template: ' + name));
137 }
f992dcf6 138
39bf2a98 139 return templateCache[searchKey];
f992dcf6
DP
140 };
141
9bdcf579
DW
142 /**
143 * Render image icons.
144 *
145 * @method pixHelper
146 * @private
8d00afb1 147 * @param {object} context The mustache context
9bdcf579 148 * @param {string} sectionText The text to parse arguments from.
e37d53da 149 * @param {function} helper Used to render the alt attribute of the text.
9bdcf579
DW
150 * @return {string}
151 */
8d00afb1 152 Renderer.prototype.pixHelper = function(context, sectionText, helper) {
9bdcf579
DW
153 var parts = sectionText.split(',');
154 var key = '';
155 var component = '';
156 var text = '';
157 var result;
158
159 if (parts.length > 0) {
160 key = parts.shift().trim();
161 }
162 if (parts.length > 0) {
163 component = parts.shift().trim();
164 }
165 if (parts.length > 0) {
166 text = parts.join(',').trim();
167 }
168 var url = coreurl.imageUrl(key, component);
169
170 var templatecontext = {
171 attributes: [
9f5f3dcc
DP
172 {name: 'src', value: url},
173 {name: 'alt', value: helper(text)},
8354d12d 174 {name: 'title', value: helper(text)},
9f5f3dcc 175 {name: 'class', value: 'smallicon'}
9bdcf579
DW
176 ]
177 };
178 // We forced loading of this early, so it will be in the cache.
39bf2a98
DW
179 var searchKey = this.currentThemeName + '/core/pix_icon';
180 var template = templateCache[searchKey];
8d00afb1 181 result = mustache.render(template, templatecontext, this.partialHelper.bind(this));
9bdcf579
DW
182 return result.trim();
183 };
184
9bdcf579
DW
185 /**
186 * Render blocks of javascript and save them in an array.
187 *
188 * @method jsHelper
189 * @private
8d00afb1 190 * @param {object} context The current mustache context.
9bdcf579
DW
191 * @param {string} sectionText The text to save as a js block.
192 * @param {function} helper Used to render the block.
193 * @return {string}
194 */
8d00afb1
DW
195 Renderer.prototype.jsHelper = function(context, sectionText, helper) {
196 this.requiredJS.push(helper(sectionText, context));
9bdcf579
DW
197 return '';
198 };
199
200 /**
201 * String helper used to render {{#str}}abd component { a : 'fish'}{{/str}}
202 * into a get_string call.
203 *
204 * @method stringHelper
205 * @private
8d00afb1 206 * @param {object} context The current mustache context.
9bdcf579
DW
207 * @param {string} sectionText The text to parse the arguments from.
208 * @param {function} helper Used to render subsections of the text.
209 * @return {string}
210 */
8d00afb1 211 Renderer.prototype.stringHelper = function(context, sectionText, helper) {
9bdcf579
DW
212 var parts = sectionText.split(',');
213 var key = '';
214 var component = '';
215 var param = '';
216 if (parts.length > 0) {
217 key = parts.shift().trim();
218 }
219 if (parts.length > 0) {
220 component = parts.shift().trim();
221 }
222 if (parts.length > 0) {
223 param = parts.join(',').trim();
224 }
225
226 if (param !== '') {
227 // Allow variable expansion in the param part only.
8d00afb1 228 param = helper(param, context);
9bdcf579
DW
229 }
230 // Allow json formatted $a arguments.
231 if ((param.indexOf('{') === 0) && (param.indexOf('{{') !== 0)) {
232 param = JSON.parse(param);
233 }
234
8d00afb1
DW
235 var index = this.requiredStrings.length;
236 this.requiredStrings.push({key: key, component: component, param: param});
a89cf237
FM
237
238 // The placeholder must not use {{}} as those can be misinterpreted by the engine.
239 return '[[_s' + index + ']]';
9bdcf579
DW
240 };
241
0b4bff8c
AN
242 /**
243 * Quote helper used to wrap content in quotes, and escape all quotes present in the content.
244 *
245 * @method quoteHelper
246 * @private
8d00afb1 247 * @param {object} context The current mustache context.
0b4bff8c
AN
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 */
8d00afb1
DW
252 Renderer.prototype.quoteHelper = function(context, sectionText, helper) {
253 var content = helper(sectionText.trim(), context);
0b4bff8c
AN
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
9bdcf579
DW
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 */
8d00afb1
DW
273 Renderer.prototype.addHelpers = function(context, themeName) {
274 this.currentThemeName = themeName;
275 this.requiredStrings = [];
276 this.requiredJS = [];
277 context.uniqid = (uniqInstances++);
c96f55e6 278 context.str = function() {
8d00afb1
DW
279 return this.stringHelper.bind(this, context);
280 }.bind(this);
c96f55e6 281 context.pix = function() {
8d00afb1
DW
282 return this.pixHelper.bind(this, context);
283 }.bind(this);
c96f55e6 284 context.js = function() {
8d00afb1
DW
285 return this.jsHelper.bind(this, context);
286 }.bind(this);
c96f55e6 287 context.quote = function() {
8d00afb1
DW
288 return this.quoteHelper.bind(this, context);
289 }.bind(this);
9f5f3dcc 290 context.globals = {config: config};
9bdcf579
DW
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 */
8d00afb1 302 Renderer.prototype.getJS = function(strings) {
9bdcf579 303 var js = '';
8d00afb1
DW
304 if (this.requiredJS.length > 0) {
305 js = this.requiredJS.join(";\n");
9bdcf579
DW
306 }
307
9bdcf579 308 // Re-render to get the final strings.
8d00afb1 309 return this.treatStringsInContent(js, strings);
29879f8f
FM
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 */
8d00afb1 329 Renderer.prototype.treatStringsInContent = function(content, strings) {
a89cf237 330 var pattern = /\[\[_s\d+\]\]/,
29879f8f
FM
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 = '';
a89cf237 347 walker = 4; // 4 is the length of '[[_s'.
29879f8f
FM
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);
a89cf237 355 } while (char != ']');
29879f8f
FM
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') {
a89cf237 360 Log.debug('Could not find string for pattern [[_s' + strIndex + ']].');
29879f8f
FM
361 strFinal = '';
362 }
363 treated += strFinal;
a89cf237 364 content = content.substr(6 + strIndex.length); // 6 is the length of the placeholder without the index: '[[_s]]'.
29879f8f
FM
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;
9bdcf579
DW
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 */
8d00afb1 391 Renderer.prototype.doRender = function(templateSource, context, themeName) {
8d00afb1 392 this.currentThemeName = themeName;
9bdcf579 393
39bf2a98 394 return this.getTemplate('core/pix_icon').then(function() {
f20a336b
DW
395 this.addHelpers(context, themeName);
396 var result = mustache.render(templateSource, context, this.partialHelper.bind(this));
397
398 if (this.requiredStrings.length > 0) {
399 return str.get_strings(this.requiredStrings).then(function(strings) {
400
401 // Why do we not do another call the render here?
402 //
403 // Because that would expose DOS holes. E.g.
404 // I create an assignment called "{{fish" which
405 // would get inserted in the template in the first pass
406 // and cause the template to die on the second pass (unbalanced).
407
408 result = this.treatStringsInContent(result, strings);
409 return $.Deferred().resolve(result, this.getJS(strings)).promise();
410 }.bind(this));
411 } else {
412 return $.Deferred().resolve(result.trim(), this.getJS([])).promise();
413 }
414 }.bind(this));
9bdcf579
DW
415 };
416
28de7771
DW
417 /**
418 * Execute a block of JS returned from a template.
419 * Call this AFTER adding the template HTML into the DOM so the nodes can be found.
420 *
421 * @method runTemplateJS
422 * @param {string} source - A block of javascript.
423 */
424 var runTemplateJS = function(source) {
425 if (source.trim() !== '') {
35be5826 426 var newscript = $('<script>').attr('type', 'text/javascript').html(source);
28de7771
DW
427 $('head').append(newscript);
428 }
429 };
430
431 /**
432 * Do some DOM replacement and trigger correct events and fire javascript.
433 *
434 * @method domReplace
435 * @private
436 * @param {JQuery} element - Element or selector to replace.
437 * @param {String} newHTML - HTML to insert / replace.
438 * @param {String} newJS - Javascript to run after the insertion.
439 * @param {Boolean} replaceChildNodes - Replace only the childnodes, alternative is to replace the entire node.
440 */
441 var domReplace = function(element, newHTML, newJS, replaceChildNodes) {
442 var replaceNode = $(element);
443 if (replaceNode.length) {
444 // First create the dom nodes so we have a reference to them.
445 var newNodes = $(newHTML);
1fca8a7b 446 var yuiNodes = null;
28de7771
DW
447 // Do the replacement in the page.
448 if (replaceChildNodes) {
1fca8a7b
DW
449 // Cleanup any YUI event listeners attached to any of these nodes.
450 yuiNodes = new Y.NodeList(replaceNode.children().get());
451 yuiNodes.destroy(true);
452
453 // JQuery will cleanup after itself.
28de7771
DW
454 replaceNode.empty();
455 replaceNode.append(newNodes);
456 } else {
1fca8a7b
DW
457 // Cleanup any YUI event listeners attached to any of these nodes.
458 yuiNodes = new Y.NodeList(replaceNode.get());
459 yuiNodes.destroy(true);
460
461 // JQuery will cleanup after itself.
28de7771
DW
462 replaceNode.replaceWith(newNodes);
463 }
464 // Run any javascript associated with the new HTML.
465 runTemplateJS(newJS);
466 // Notify all filters about the new content.
467 event.notifyFilterContentUpdated(newNodes);
468 }
469 };
470
39bf2a98
DW
471 /**
472 * Scan a template source for partial tags and return a list of the found partials.
473 *
474 * @method scanForPartials
475 * @private
476 * @param {string} templateSource - source template to scan.
477 * @return {Array} List of partials.
478 */
479 Renderer.prototype.scanForPartials = function(templateSource) {
480 var tokens = mustache.parse(templateSource),
481 partials = [];
482
483 var findPartial = function(tokens, partials) {
484 var i, token;
485 for (i = 0; i < tokens.length; i++) {
486 token = tokens[i];
487 if (token[0] == '>' || token[0] == '<') {
488 partials.push(token[1]);
489 }
490 if (token.length > 4) {
491 findPartial(token[4], partials);
492 }
493 }
494 };
495
496 findPartial(tokens, partials);
497
498 return partials;
499 };
500
501 /**
502 * Load a template and scan it for partials. Recursively fetch the partials.
503 *
504 * @method cachePartials
505 * @private
506 * @param {string} templateName - should consist of the component and the name of the template like this:
507 * core/menu (lib/templates/menu.mustache) or
508 * tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache)
509 * @return {Promise} JQuery promise object resolved when all partials are in the cache.
510 */
511 Renderer.prototype.cachePartials = function(templateName) {
b302369d 512 return this.getTemplate(templateName).then(function(templateSource) {
39bf2a98
DW
513 var i;
514 var partials = this.scanForPartials(templateSource);
515 var fetchThemAll = [];
516
517 for (i = 0; i < partials.length; i++) {
518 var searchKey = this.currentThemeName + '/' + partials[i];
519 if (searchKey in templatePromises) {
520 continue;
521 }
522 fetchThemAll.push(this.cachePartials(partials[i]));
523 }
524
f3cd5c5b 525 return $.when.apply($, fetchThemAll).then(function() {
39bf2a98 526 return templateSource;
b302369d 527 });
39bf2a98
DW
528 }.bind(this));
529 };
530
8d00afb1
DW
531 /**
532 * Load a template and call doRender on it.
533 *
534 * @method render
535 * @private
536 * @param {string} templateName - should consist of the component and the name of the template like this:
537 * core/menu (lib/templates/menu.mustache) or
538 * tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache)
539 * @param {Object} context - Could be array, string or simple value for the context of the template.
540 * @param {string} themeName - Name of the current theme.
541 * @return {Promise} JQuery promise object resolved when the template has been rendered.
542 */
543 Renderer.prototype.render = function(templateName, context, themeName) {
8d00afb1
DW
544 if (typeof (themeName) === "undefined") {
545 // System context by default.
546 themeName = config.theme;
547 }
548
549 this.currentThemeName = themeName;
550
b302369d 551 return this.cachePartials(templateName).then(function(templateSource) {
f20a336b 552 return this.doRender(templateSource, context, themeName);
b302369d 553 }.bind(this));
8d00afb1
DW
554 };
555
90525930
MN
556 /**
557 * Prepend some HTML to a node and trigger events and fire javascript.
558 *
559 * @method domPrepend
560 * @private
561 * @param {jQuery|String} element - Element or selector to prepend HTML to
562 * @param {String} html - HTML to prepend
563 * @param {String} js - Javascript to run after we prepend the html
564 */
565 var domPrepend = function(element, html, js) {
566 var node = $(element);
567 if (node.length) {
568 // Prepend the html.
569 node.prepend(html);
570 // Run any javascript associated with the new HTML.
571 runTemplateJS(js);
572 // Notify all filters about the new content.
573 event.notifyFilterContentUpdated(node);
574 }
575 };
28de7771 576
f7775c9a
MN
577 /**
578 * Append some HTML to a node and trigger events and fire javascript.
579 *
580 * @method domAppend
581 * @private
582 * @param {jQuery|String} element - Element or selector to append HTML to
583 * @param {String} html - HTML to append
584 * @param {String} js - Javascript to run after we append the html
585 */
586 var domAppend = function(element, html, js) {
587 var node = $(element);
588 if (node.length) {
589 // Append the html.
590 node.append(html);
591 // Run any javascript associated with the new HTML.
592 runTemplateJS(js);
593 // Notify all filters about the new content.
594 event.notifyFilterContentUpdated(node);
595 }
596 };
597
9bdcf579
DW
598 return /** @alias module:core/templates */ {
599 // Public variables and functions.
600 /**
8d00afb1
DW
601 * Every call to render creates a new instance of the class and calls render on it. This
602 * means each render call has it's own class variables.
9bdcf579
DW
603 *
604 * @method render
605 * @private
606 * @param {string} templateName - should consist of the component and the name of the template like this:
607 * core/menu (lib/templates/menu.mustache) or
608 * tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache)
609 * @param {Object} context - Could be array, string or simple value for the context of the template.
610 * @param {string} themeName - Name of the current theme.
611 * @return {Promise} JQuery promise object resolved when the template has been rendered.
612 */
613 render: function(templateName, context, themeName) {
8d00afb1
DW
614 var renderer = new Renderer();
615 return renderer.render(templateName, context, themeName);
9bdcf579
DW
616 },
617
618 /**
619 * Execute a block of JS returned from a template.
620 * Call this AFTER adding the template HTML into the DOM so the nodes can be found.
621 *
622 * @method runTemplateJS
9bdcf579
DW
623 * @param {string} source - A block of javascript.
624 */
28de7771
DW
625 runTemplateJS: runTemplateJS,
626
627 /**
628 * Replace a node in the page with some new HTML and run the JS.
629 *
630 * @method replaceNodeContents
c96f55e6
DP
631 * @param {JQuery} element - Element or selector to replace.
632 * @param {String} newHTML - HTML to insert / replace.
633 * @param {String} newJS - Javascript to run after the insertion.
28de7771
DW
634 */
635 replaceNodeContents: function(element, newHTML, newJS) {
c96f55e6 636 domReplace(element, newHTML, newJS, true);
28de7771
DW
637 },
638
639 /**
640 * Insert a node in the page with some new HTML and run the JS.
641 *
642 * @method replaceNode
c96f55e6
DP
643 * @param {JQuery} element - Element or selector to replace.
644 * @param {String} newHTML - HTML to insert / replace.
645 * @param {String} newJS - Javascript to run after the insertion.
28de7771
DW
646 */
647 replaceNode: function(element, newHTML, newJS) {
c96f55e6 648 domReplace(element, newHTML, newJS, false);
f7775c9a
MN
649 },
650
90525930
MN
651 /**
652 * Prepend some HTML to a node and trigger events and fire javascript.
653 *
654 * @method prependNodeContents
655 * @param {jQuery|String} element - Element or selector to prepend HTML to
656 * @param {String} html - HTML to prepend
657 * @param {String} js - Javascript to run after we prepend the html
658 */
659 prependNodeContents: function(element, html, js) {
660 domPrepend(element, html, js);
661 },
662
f7775c9a
MN
663 /**
664 * Append some HTML to a node and trigger events and fire javascript.
665 *
666 * @method appendNodeContents
667 * @param {jQuery|String} element - Element or selector to append HTML to
668 * @param {String} html - HTML to append
669 * @param {String} js - Javascript to run after we append the html
670 */
671 appendNodeContents: function(element, html, js) {
672 domAppend(element, html, js);
9bdcf579
DW
673 }
674 };
675});