PageRenderTime 91ms CodeModel.GetById 26ms RepoModel.GetById 1ms app.codeStats 0ms

/utils/tmpl.js

https://github.com/winhamwr/khan-exercises
JavaScript | 573 lines | 347 code | 109 blank | 117 comment | 83 complexity | 2fa23e97f75938bc453681bc4a700093 MD5 | raw file
  1. (function() {
  2. // Keep the template variables private, to prevent external access
  3. var VARS = {};
  4. jQuery.tmpl = {
  5. // Processors that act based on element attributes
  6. attr: {
  7. "data-ensure": function( elem, ensure ) {
  8. // Returns a function in order to run after other templating and var assignment
  9. return function( elem ) {
  10. // Return a boolean corresponding to the ensure's value
  11. // False means all templating will be run again, so new values will be chosen
  12. return !!(ensure && jQuery.tmpl.getVAR( ensure ));
  13. };
  14. },
  15. "data-if": function( elem, value ) {
  16. var $elem = jQuery( elem );
  17. // Check if the attribute should be deleted
  18. if ( $elem.data( "toDelete" ) ) {
  19. $elem.removeAttr( "data-if" ).removeData( "if" );
  20. }
  21. value = value && jQuery.tmpl.getVAR( value );
  22. // Save the result of this data-if in the next sibling for data-else-if and data-else
  23. $elem.next().data( "lastCond", value );
  24. if ( !value ) {
  25. // Delete the element if the data-if evaluated to false
  26. return [];
  27. }
  28. },
  29. "data-else-if": function( elem, value ) {
  30. var $elem = jQuery( elem );
  31. // Check if the attribute should be deleted
  32. if ( $elem.data( "toDelete" ) ) {
  33. $elem.removeAttr( "data-else-if" ).removeData( "elseIf" );
  34. }
  35. var lastCond = $elem.data( "lastCond" );
  36. // Show this element iff the preceding element was hidden AND this data-if returns truthily
  37. value = !lastCond && value && jQuery.tmpl.getVAR( value );
  38. // Succeeding elements care about the visibility of both me and my preceding siblings
  39. $elem.next().data( "lastCond", lastCond || value );
  40. if ( !value ) {
  41. // Delete the element if appropriate
  42. return [];
  43. }
  44. },
  45. "data-else": function( elem ) {
  46. var $elem = jQuery( elem );
  47. // Check if the attribute should be deleted
  48. if ( $elem.data( "toDelete" ) ) {
  49. $elem.removeAttr( "data-else" ).removeData( "else" );
  50. }
  51. if ( $elem.data( "lastCond" ) ) {
  52. // Delete the element if the data-if of the preceding element was true
  53. return [];
  54. }
  55. },
  56. "data-each": function( elem, value ) {
  57. var match;
  58. // Remove the data-each attribute so it doesn't end up in the generated elements
  59. jQuery( elem ).removeAttr( "data-each" );
  60. // Extract the 1, 2, or 3 parts of the data-each attribute, which could be
  61. // - items
  62. // - items as value
  63. // - items as pos, value
  64. if ( (match = /^(.*?)(?: as (?:(\w+), )?(\w+))?$/.exec( value )) ) {
  65. // See "if ( ret.items )" in traverse() for the other half of the data-each code
  66. return {
  67. // The collection which we'll iterate through
  68. items: jQuery.tmpl.getVAR( match[1] ),
  69. // "value" and "pos" as strings
  70. value: match[3],
  71. pos: match[2],
  72. // Save the values of the iterator variables so we don't permanently overwrite them
  73. oldValue: VARS[ match[3] ],
  74. oldPos: VARS[ match[2] ]
  75. };
  76. }
  77. },
  78. "data-unwrap": function( elem ) {
  79. return jQuery( elem ).contents();
  80. }
  81. },
  82. // Processors that act based on tag names
  83. type: {
  84. "var": function( elem, value ) {
  85. // When called by process(), value is undefined
  86. // If the <var> has any child elements, run later with the innerHTML
  87. // Use jQuery instead of getElementsByTagName to exclude comment nodes in IE
  88. if ( !value && jQuery( elem ).children().length > 0 ) {
  89. return function( elem ) {
  90. return jQuery.tmpl.type["var"]( elem, elem.innerHTML );
  91. };
  92. }
  93. // Evaluate the contents of the <var> as a JS string
  94. value = value || jQuery.tmpl.getVAR( elem );
  95. // If an ID was specified then we're going to save the value
  96. var name = elem.id;
  97. if ( name ) {
  98. // Utility function for VARS[ name ] = value, warning if the name overshadows a KhanUtil property
  99. function setVAR( name, value ) {
  100. if ( KhanUtil[ name ] ) {
  101. Khan.error( "Defining variable '" + name + "' overwrites utility property of same name." );
  102. }
  103. VARS[ name ] = value;
  104. }
  105. // Destructure the array if appropriate
  106. if ( name.indexOf( "," ) !== -1 ) {
  107. // Nested arrays are not supported
  108. var parts = name.split(/\s*,\s*/);
  109. jQuery.each( parts, function( i, part ) {
  110. // Ignore empty parts
  111. if ( part.length > 0 ) {
  112. setVAR( part, value[i] );
  113. }
  114. });
  115. // Just a normal assignment
  116. } else {
  117. setVAR( name, value );
  118. }
  119. // No value was specified so we replace it with a text node of the value
  120. } else {
  121. if ( value == null ) {
  122. // Don't show anything
  123. return [];
  124. } else {
  125. // Convert the value to a string and replace with those elements and text nodes
  126. // Add a space so that it can end with a "<" in Safari
  127. var div = jQuery( "<div>" );
  128. var html = div.append( value + " " ).html();
  129. return div.html( html.slice( 0, -1 ) ).contents();
  130. }
  131. }
  132. },
  133. code: function( elem ) {
  134. // Returns a function in order to run after other templating and var assignment
  135. return function( elem ) {
  136. if ( typeof elem.MathJax === "undefined" ) {
  137. var $elem = jQuery( elem );
  138. // Maintain the classes from the original element
  139. if ( elem.className ) {
  140. $elem.wrap( "<span class='" + elem.className + "'></span>" );
  141. }
  142. // Trick MathJax into thinking that we're dealing with a script block
  143. elem.type = "math/tex";
  144. // Make sure that the old value isn't being displayed anymore
  145. elem.style.display = "none";
  146. // Clean up any strange mathematical expressions
  147. var text = $elem.text();
  148. $elem.text( KhanUtil.cleanMath ? KhanUtil.cleanMath( text ) : text );
  149. // Stick the processing request onto the queue
  150. if ( typeof MathJax !== "undefined" ) {
  151. MathJax.Hub.Queue([ "Typeset", MathJax.Hub, elem ]);
  152. }
  153. } else {
  154. MathJax.Hub.Queue([ "Reprocess", MathJax.Hub, elem ]);
  155. }
  156. };
  157. }
  158. },
  159. // Eval a string in the context of Math, KhanUtil, VARS, and optionally another passed context
  160. getVAR: function( elem, ctx ) {
  161. // We need to compute the value
  162. var code = elem.nodeName ? jQuery(elem).text() : elem;
  163. // Make sure any HTML formatting is stripped
  164. code = jQuery.trim( jQuery.tmpl.cleanHTML( code ) );
  165. // If no extra context was passed, use an empty object
  166. if ( ctx == null ) {
  167. ctx = {};
  168. }
  169. try {
  170. // Use the methods from JavaScript's built-in Math methods
  171. with ( Math ) {
  172. // And the methods provided by the library
  173. with ( KhanUtil ) {
  174. // And the passed-in context
  175. with ( ctx ) {
  176. // And all the computed variables
  177. with ( VARS ) {
  178. return eval( "(function() { return (" + code + "); })()" );
  179. }
  180. }
  181. }
  182. }
  183. } catch ( e ) {
  184. var info;
  185. if ( elem.nodeName ) {
  186. info = elem.nodeName.toLowerCase();
  187. if ( elem.id != null && elem.id.length > 0 ) {
  188. info += "#" + elem.id;
  189. }
  190. } else {
  191. info = JSON.stringify( code );
  192. }
  193. Khan.error( "Error while evaluating " + info, e );
  194. }
  195. },
  196. // Make sure any HTML formatting is stripped
  197. cleanHTML: function( text ) {
  198. return text.replace(/&gt;/g, ">").replace(/&lt;/g, "<").replace(/&amp;/g, "&");
  199. }
  200. };
  201. if ( typeof KhanUtil !== "undefined" ) {
  202. KhanUtil.tmpl = jQuery.tmpl;
  203. }
  204. // Reinitialize VARS for each problem
  205. jQuery.fn.tmplLoad = function( problem, info ) {
  206. VARS = {};
  207. // Check to see if we're in test mode
  208. if ( info.testMode ) {
  209. // Expose the variables if we're in test mode
  210. jQuery.tmpl.VARS = VARS;
  211. }
  212. };
  213. jQuery.fn.tmplCleanup = function() {
  214. this.find( "code" ).each( function() {
  215. MathJax.Hub.getJaxFor( this ).Remove();
  216. } );
  217. };
  218. jQuery.fn.tmpl = function() {
  219. // Call traverse() for each element in the jQuery object
  220. for ( var i = 0, l = this.length; i < l; i++ ) {
  221. traverse( this[i] );
  222. }
  223. return this;
  224. // Walk through the element and its descendants, process()-ing each one using the processors defined above
  225. function traverse( elem ) {
  226. // Array of functions to run after doing the rest of the processing
  227. var post = [],
  228. // Live NodeList of child nodes to traverse if we don't remove/replace this element
  229. child = elem.childNodes,
  230. // Result of running the attribute and tag processors on the element
  231. ret = process( elem, post );
  232. // If false, rerun all templating (like data-ensure)
  233. if ( ret === false ) {
  234. return traverse( elem );
  235. // If undefined, do nothing
  236. } else if ( ret === undefined ) {
  237. ;
  238. // If a (possibly-empty) array of nodes, replace this one with those
  239. // The type of ret is checked to ensure it is not a function
  240. } else if ( typeof ret === "object" && typeof ret.length !== "undefined" ) {
  241. if ( elem.parentNode ) {
  242. // All nodes must be inserted before any are traversed
  243. jQuery.each( ret, function( i, rep ) {
  244. if ( rep.nodeType ) {
  245. elem.parentNode.insertBefore( rep, elem );
  246. }
  247. } );
  248. jQuery.each( ret, function( i, rep ) {
  249. traverse( rep );
  250. } );
  251. elem.parentNode.removeChild( elem );
  252. }
  253. return null;
  254. // If { items: ... }, this is a data-each loop
  255. } else if ( ret.items ) {
  256. // We need these references to insert the elements in the appropriate places
  257. var origParent = elem.parentNode,
  258. origNext = elem.nextSibling;
  259. // Loop though the given array
  260. jQuery.each( ret.items, function( pos, value ) {
  261. // Set the value if appropriate
  262. if ( ret.value ) {
  263. VARS[ ret.value ] = value;
  264. }
  265. // Set the position if appropriate
  266. if ( ret.pos ) {
  267. VARS[ ret.pos ] = pos;
  268. }
  269. // Do a deep clone (including event handlers and data) of the element
  270. var clone = jQuery( elem ).clone( true )
  271. .removeAttr( "data-each" ).removeData( "each" )[0];
  272. // Flag elements with the following attributes so that the attributes can be removed after templating
  273. jQuery( clone ).find("[data-if], [data-else-if], [data-else]").each(function() {
  274. jQuery( this ).data( "toDelete", true );
  275. });
  276. // Insert in the proper place (depends on whether the loops is the last of its siblings)
  277. if ( origNext ) {
  278. origParent.insertBefore( clone, origNext );
  279. } else {
  280. origParent.appendChild( clone );
  281. }
  282. // Run all templating on the new element
  283. traverse( clone );
  284. });
  285. // Restore the old value of the value variable, if it had one
  286. if ( ret.value ) {
  287. VARS[ ret.value ] = ret.oldValue;
  288. }
  289. // Restore the old value of the position variable, if it had one
  290. if ( ret.pos ) {
  291. VARS[ ret.pos ] = ret.oldPos;
  292. }
  293. // Remove the loop element and its handlers now that we've processed it
  294. jQuery( elem ).remove();
  295. // Say that the element was removed so that child traversal doesn't skip anything
  296. return null;
  297. }
  298. // Loop through the element's children if it was not removed
  299. for ( var i = 0; i < child.length; i++ ) {
  300. // Traverse the child; decrement the counter if the child was removed
  301. if ( child[i].nodeType === 1 && traverse( child[i] ) === null ) {
  302. i--;
  303. }
  304. }
  305. // Run through each post-processing function
  306. for ( var i = 0, l = post.length; i < l; i++ ) {
  307. // If false, rerun all templating (for data-ensure and <code> math)
  308. if ( post[i]( elem ) === false ) {
  309. return traverse( elem );
  310. }
  311. }
  312. return elem;
  313. }
  314. // Run through the attr and type processors, return as soon as one of them is decisive about a plan of action
  315. function process( elem, post ) {
  316. var ret, newElem,
  317. $elem = jQuery( elem );
  318. // Look through each of the attr processors, see if our element has the matching attribute
  319. for ( var attr in jQuery.tmpl.attr ) {
  320. var value;
  321. if ( ( /^data-/ ).test( attr ) ) {
  322. value = $elem.data( attr.replace( /^data-/, "" ) );
  323. } else {
  324. value = $elem.attr( attr );
  325. }
  326. if ( value !== undefined ) {
  327. ret = jQuery.tmpl.attr[ attr ]( elem, value );
  328. // If a function, run after all of the other templating
  329. if ( typeof ret === "function" ) {
  330. post.push( ret );
  331. // Return anything else (boolean, array of nodes for replacement, object for data-each)
  332. } else if ( ret !== undefined ) {
  333. return ret;
  334. }
  335. }
  336. }
  337. // Look up the processor based on the tag name
  338. var type = elem.nodeName.toLowerCase();
  339. if ( jQuery.tmpl.type[ type ] != null ) {
  340. ret = jQuery.tmpl.type[ type ]( elem );
  341. // If a function, run after all of the other templating
  342. if ( typeof ret === "function" ) {
  343. post.push( ret );
  344. }
  345. }
  346. return ret;
  347. }
  348. };
  349. jQuery.extend( jQuery.expr[":"], {
  350. inherited: function(el) {
  351. return jQuery( el ).data( "inherited" );
  352. }
  353. } );
  354. jQuery.fn.extend({
  355. tmplApply: function( options ) {
  356. options = options || {};
  357. // Get the attribute which we'll be checking, defaults to "id"
  358. // but "class" is sometimes used
  359. var attribute = options.attribute || "id",
  360. // Figure out the way in which the application will occur
  361. defaultApply = options.defaultApply || "replace",
  362. // Store for elements to be used later
  363. parent = {};
  364. return this.each(function() {
  365. var $this = jQuery( this ),
  366. name = $this.attr( attribute ),
  367. hint = $this.data( "apply" ) && !$this.data( "apply" ).indexOf( "hint" );
  368. // Only operate on the element if it has the attribute that we're using
  369. if ( name ) {
  370. // The inheritance only works if we've seen an element already
  371. // that matches the particular name and we're not looking at hint
  372. // templating
  373. if ( name in parent && !hint ) {
  374. // Get the method through which we'll be doing the application
  375. // You can specify an application style directly on the sub-element
  376. parent[ name ] = jQuery.tmplApplyMethods[ $this.data( "apply" ) || defaultApply ]
  377. // Call it with the context of the parent and the sub-element itself
  378. .call( parent[ name ], this );
  379. if ( parent[ name ] == null ) {
  380. delete parent[ name ];
  381. }
  382. // Store the parent element for later use if it was inherited from somewhere else
  383. } else if ( $this.closest( ":inherited" ).length > 0 ) {
  384. parent[ name ] = this;
  385. }
  386. }
  387. });
  388. }
  389. });
  390. jQuery.extend({
  391. // These methods should be called with context being the parent
  392. // and first argument being the child.
  393. tmplApplyMethods: {
  394. // Removes both the parent and the child
  395. remove: function( elem ) {
  396. jQuery( this ).remove();
  397. jQuery( elem ).remove();
  398. },
  399. // Replaces the parent with the child
  400. replace: function( elem ) {
  401. jQuery( this ).replaceWith( elem );
  402. return elem;
  403. },
  404. // Replaces the parent with the child's content. Useful when
  405. // needed to replace an element without introducing additional
  406. // wrappers.
  407. splice: function( elem ) {
  408. jQuery( this ).replaceWith( jQuery( elem ).contents() );
  409. },
  410. // Appends the child element to the parent element
  411. append: function( elem ) {
  412. jQuery( this ).append( elem );
  413. return this;
  414. },
  415. // Appends the child element's contents to the parent element.
  416. appendContents: function( elem ) {
  417. jQuery( this ).append( jQuery( elem ).contents() );
  418. jQuery( elem ).remove();
  419. return this;
  420. },
  421. // Prepends the child element to the parent.
  422. prepend: function( elem ) {
  423. jQuery( this ).prepend( elem );
  424. return this;
  425. },
  426. // Prepends the child element's contents to the parent element.
  427. prependContents: function( elem ) {
  428. jQuery( this ).prepend( jQuery( elem ).contents() );
  429. jQuery( elem ).remove();
  430. return this;
  431. },
  432. // Insert child before the parent.
  433. before: function( elem ) {
  434. jQuery( this ).before( elem );
  435. return this;
  436. },
  437. // Insert child's contents before the parent.
  438. beforeContents: function( elem ) {
  439. jQuery( this ).before( jQuery( elem ).contents() );
  440. jQuery( elem ).remove();
  441. return this;
  442. },
  443. // Insert child after the parent.
  444. after: function( elem ) {
  445. jQuery( this ).after( elem );
  446. return this;
  447. },
  448. // Insert child's contents after the parent.
  449. afterContents: function( elem ) {
  450. jQuery( this ).after( jQuery( elem ).contents() );
  451. jQuery( elem ).remove();
  452. return this;
  453. },
  454. // Like appendContents but also merges the data-ensures
  455. appendVars: function( elem ) {
  456. var parentEnsure = jQuery( this ).data("ensure") || "1";
  457. var childEnsure = jQuery( elem ).data("ensure") || "1";
  458. jQuery( this ).data("ensure",
  459. "(" + parentEnsure + ") && (" + childEnsure + ")");
  460. return jQuery.tmplApplyMethods.appendContents.call( this, elem );
  461. }
  462. }
  463. });
  464. })();