1 /** 2 * @fileoverview This file contains miscellaneous basic functionality. 3 * 4 */ 5 6 /** 7 * Creates a DOM element with the given tag name in the document of the 8 * owner element. 9 * 10 * @param {String} tagName The name of the tag to create. 11 * @param {Element} owner The intended owner (i.e., parent element) of 12 * the created element. 13 * @param {Point} opt_position The top-left corner of the created element. 14 * @param {Size} opt_size The size of the created element. 15 * @param {Boolean} opt_noAppend Do not append the new element to the owner. 16 * @return {Element} The newly created element node. 17 */ 18 function createElement(tagName, owner, opt_position, opt_size, opt_noAppend) { 19 var element = ownerDocument(owner).createElement(tagName); 20 if (opt_position) { 21 setPosition(element, opt_position); 22 } 23 if (opt_size) { 24 setSize(element, opt_size); 25 } 26 if (owner && !opt_noAppend) { 27 appendChild(owner, element); 28 } 29 30 return element; 31 } 32 33 /** 34 * Creates a text node with the given value. 35 * 36 * @param {String} value The text to place in the new node. 37 * @param {Element} owner The owner (i.e., parent element) of the new 38 * text node. 39 * @return {Text} The newly created text node. 40 */ 41 function createTextNode(value, owner) { 42 var element = ownerDocument(owner).createTextNode(value); 43 if (owner) { 44 appendChild(owner, element); 45 } 46 return element; 47 } 48 49 /** 50 * Returns the document owner of the given element. In particular, 51 * returns window.document if node is null or the browser does not 52 * support ownerDocument. 53 * 54 * @param {Node} node The node whose ownerDocument is required. 55 * @returns {Document|Null} The owner document or null if unsupported. 56 */ 57 function ownerDocument(node) { 58 return (node ? node.ownerDocument : null) || document; 59 } 60 61 /** 62 * Wrapper function to create CSS units (pixels) string 63 * 64 * @param {Number} numPixels Number of pixels, may be floating point. 65 * @returns {String} Corresponding CSS units string. 66 */ 67 function px(numPixels) { 68 return round(numPixels) + "px"; 69 } 70 71 /** 72 * Sets the left and top of the given element to the given point. 73 * 74 * @param {Element} element The dom element to manipulate. 75 * @param {Point} point The desired position. 76 */ 77 function setPosition(element, point) { 78 var style = element.style; 79 style.position = "absolute"; 80 style.left = px(point.x); 81 style.top = px(point.y); 82 } 83 84 /** 85 * Sets the width and height style attributes to the given size. 86 * 87 * @param {Element} element The dom element to manipulate. 88 * @param {Size} size The desired size. 89 */ 90 function setSize(element, size) { 91 var style = element.style; 92 style.width = px(size.width); 93 style.height = px(size.height); 94 } 95 96 /** 97 * Sets display to none. Doing this as a function saves a few bytes for 98 * the 'style.display' property and the 'none' literal. 99 * 100 * @param {Element} node The dom element to manipulate. 101 */ 102 function displayNone(node) { 103 node.style.display = 'none'; 104 } 105 106 /** 107 * Sets display to default. 108 * 109 * @param {Element} node The dom element to manipulate. 110 */ 111 function displayDefault(node) { 112 node.style.display = ''; 113 } 114 115 /** 116 * Appends the given child to the given parent in the DOM 117 * 118 * @param {Element} parent The parent dom element. 119 * @param {Node} child The new child dom node. 120 */ 121 function appendChild(parent, child) { 122 parent.appendChild(child); 123 } 124 125 126 /** 127 * Wrapper for the eval() builtin function to evaluate expressions and 128 * obtain their value. It wraps the expression in parentheses such 129 * that object literals are really evaluated to objects. Without the 130 * wrapping, they are evaluated as block, and create syntax 131 * errors. Also protects against other syntax errors in the eval()ed 132 * code and returns null if the eval throws an exception. 133 * 134 * @param {String} expr 135 * @return {Object|Null} 136 */ 137 function jsEval(expr) { 138 try { 139 return eval('[' + expr + '][0]'); 140 } catch (e) { 141 return null; 142 } 143 } 144 145 146 /** 147 * Wrapper for the eval() builtin function to execute statements. This 148 * guards against exceptions thrown, but doesn't return a 149 * value. Still, mostly for testability, it returns a boolean to 150 * indicate whether execution was successful. NOTE: 151 * javascript's eval semantics is murky in that it confounds 152 * expression evaluation and statement execution into a single 153 * construct. Cf. jsEval(). 154 * 155 * @param {String} stmt 156 * @return {Boolean} 157 */ 158 function jsExec(stmt) { 159 try { 160 eval(stmt); 161 return true; 162 } catch (e) { 163 return false; 164 } 165 } 166 167 168 /** 169 * Wrapper for eval with a context. NOTE: The style guide 170 * deprecates eval, so this is the exception that proves the 171 * rule. Notice also that since the value of the expression is 172 * returned rather than assigned to a local variable, one major 173 * objection aganist the use of the with() statement, namely that 174 * properties of the with() target override local variables of the 175 * same name, is void here. 176 * 177 * @param {String} expr 178 * @param {Object} context 179 * @return {Object|Null} 180 */ 181 function jsEvalWith(expr, context) { 182 try { 183 with (context) { 184 return eval('[' + expr + '][0]'); 185 } 186 } catch (e) { 187 return null; 188 } 189 } 190 191 192 var DOM_ELEMENT_NODE = 1; 193 var DOM_ATTRIBUTE_NODE = 2; 194 var DOM_TEXT_NODE = 3; 195 var DOM_CDATA_SECTION_NODE = 4; 196 var DOM_ENTITY_REFERENCE_NODE = 5; 197 var DOM_ENTITY_NODE = 6; 198 var DOM_PROCESSING_INSTRUCTION_NODE = 7; 199 var DOM_COMMENT_NODE = 8; 200 var DOM_DOCUMENT_NODE = 9; 201 var DOM_DOCUMENT_TYPE_NODE = 10; 202 var DOM_DOCUMENT_FRAGMENT_NODE = 11; 203 var DOM_NOTATION_NODE = 12; 204 205 /** 206 * Traverses the element nodes in the DOM tree underneath the given 207 * node and finds the first node with elemId, or null if there is no such 208 * element. Traversal is in depth-first order. 209 * 210 * NOTE: The reason this is not combined with the elem() function is 211 * that the implementations are different. 212 * elem() is a wrapper for the built-in document.getElementById() function, 213 * whereas this function performs the traversal itself. 214 * Modifying elem() to take an optional root node is a possibility, 215 * but the in-built function would perform better than using our own traversal. 216 * 217 * @param {Element} node Root element of subtree to traverse. 218 * @param {String} elemId The id of the element to search for. 219 * @return {Element|Null} The corresponding element, or null if not found. 220 */ 221 function nodeGetElementById(node, elemId) { 222 for (var c = node.firstChild; c; c = c.nextSibling) { 223 if (c.id == elemId) { 224 return c; 225 } 226 if (c.nodeType == DOM_ELEMENT_NODE) { 227 var n = arguments.callee.call(this, c, elemId); 228 if (n) { 229 return n; 230 } 231 } 232 } 233 return null; 234 } 235 236 237 /** 238 * Get an attribute from the DOM. Simple redirect, exists to compress code. 239 * 240 * @param {Element} node Element to interrogate. 241 * @param {String} name Name of parameter to extract. 242 * @return {String} Resulting attribute. 243 */ 244 function domGetAttribute(node, name) { 245 return node.getAttribute(name); 246 } 247 248 /** 249 * Set an attribute in the DOM. Simple redirect to compress code. 250 * 251 * @param {Element} node Element to interrogate. 252 * @param {String} name Name of parameter to set. 253 * @param {String} value Set attribute to this value. 254 */ 255 function domSetAttribute(node, name, value) { 256 node.setAttribute(name, value); 257 } 258 259 /** 260 * Remove an attribute from the DOM. Simple redirect to compress code. 261 * 262 * @param {Element} node Element to interrogate. 263 * @param {String} name Name of parameter to remove. 264 */ 265 function domRemoveAttribute(node, name) { 266 node.removeAttribute(name); 267 } 268 269 /** 270 * Clone a node in the DOM. 271 * 272 * @param {Node} node Node to clone. 273 * @return {Node} Cloned node. 274 */ 275 function domCloneNode(node) { 276 return node.cloneNode(true); 277 } 278 279 280 /** 281 * Return a safe string for the className of a node. 282 * If className is not a string, returns "". 283 * 284 * @param {Element} node DOM element to query. 285 * @return {String} 286 */ 287 function domClassName(node) { 288 return node.className ? "" + node.className : ""; 289 } 290 291 /** 292 * Adds a class name to the class attribute of the given node. 293 * 294 * @param {Element} node DOM element to modify. 295 * @param {String} className Class name to add. 296 */ 297 function domAddClass(node, className) { 298 var name = domClassName(node); 299 if (name) { 300 var cn = name.split(/\s+/); 301 var found = false; 302 for (var i = 0; i < jsLength(cn); ++i) { 303 if (cn[i] == className) { 304 found = true; 305 break; 306 } 307 } 308 309 if (!found) { 310 cn.push(className); 311 } 312 313 node.className = cn.join(' '); 314 } else { 315 node.className = className; 316 } 317 } 318 319 /** 320 * Removes a class name from the class attribute of the given node. 321 * 322 * @param {Element} node DOM element to modify. 323 * @param {String} className Class name to remove. 324 */ 325 function domRemoveClass(node, className) { 326 var c = domClassName(node); 327 if (!c || c.indexOf(className) == -1) { 328 return; 329 } 330 var cn = c.split(/\s+/); 331 for (var i = 0; i < jsLength(cn); ++i) { 332 if (cn[i] == className) { 333 cn.splice(i--, 1); 334 } 335 } 336 node.className = cn.join(' '); 337 } 338 339 /** 340 * Checks if a node belongs to a style class. 341 * 342 * @param {Element} node DOM element to test. 343 * @param {String} className Class name to check for. 344 * @return {Boolean} Node belongs to style class. 345 */ 346 function domTestClass(node, className) { 347 var cn = domClassName(node).split(/\s+/); 348 for (var i = 0; i < jsLength(cn); ++i) { 349 if (cn[i] == className) { 350 return true; 351 } 352 } 353 return false; 354 } 355 356 /** 357 * Inserts a new child before a given sibling. 358 * 359 * @param {Node} newChild Node to insert. 360 * @param {Node} oldChild Sibling node. 361 * @return {Node} Reference to new child. 362 */ 363 function domInsertBefore(newChild, oldChild) { 364 return oldChild.parentNode.insertBefore(newChild, oldChild); 365 } 366 367 /** 368 * Appends a new child to the specified (parent) node. 369 * 370 * @param {Element} node Parent element. 371 * @param {Node} child Child node to append. 372 * @return {Node} Newly appended node. 373 */ 374 function domAppendChild(node, child) { 375 return node.appendChild(child); 376 } 377 378 /** 379 * Remove a new child from the specified (parent) node. 380 * 381 * @param {Element} node Parent element. 382 * @param {Node} child Child node to remove. 383 * @return {Node} Removed node. 384 */ 385 function domRemoveChild(node, child) { 386 return node.removeChild(child); 387 } 388 389 /** 390 * Replaces an old child node with a new child node. 391 * 392 * @param {Node} newChild New child to append. 393 * @param {Node} oldChild Old child to remove. 394 * @return {Node} Replaced node. 395 */ 396 function domReplaceChild(newChild, oldChild) { 397 return oldChild.parentNode.replaceChild(newChild, oldChild); 398 } 399 400 /** 401 * Removes a node from the DOM. 402 * 403 * @param {Node} node The node to remove. 404 * @return {Node} The removed node. 405 */ 406 function domRemoveNode(node) { 407 return domRemoveChild(node.parentNode, node); 408 } 409 410 /** 411 * Creates a new text node in the given document. 412 * 413 * @param {Document} doc Target document. 414 * @param {String} text Text composing new text node. 415 * @return {Text} Newly constructed text node. 416 */ 417 function domCreateTextNode(doc, text) { 418 return doc.createTextNode(text); 419 } 420 421 /** 422 * Creates a new node in the given document 423 * 424 * @param {Document} doc Target document. 425 * @param {String} name Name of new element (i.e. the tag name).. 426 * @return {Element} Newly constructed element. 427 */ 428 function domCreateElement(doc, name) { 429 return doc.createElement(name); 430 } 431 432 /** 433 * Creates a new attribute in the given document. 434 * 435 * @param {Document} doc Target document. 436 * @param {String} name Name of new attribute. 437 * @return {Attr} Newly constructed attribute. 438 */ 439 function domCreateAttribute(doc, name) { 440 return doc.createAttribute(name); 441 } 442 443 /** 444 * Creates a new comment in the given document. 445 * 446 * @param {Document} doc Target document. 447 * @param {String} text Comment text. 448 * @return {Comment} Newly constructed comment. 449 */ 450 function domCreateComment(doc, text) { 451 return doc.createComment(text); 452 } 453 454 /** 455 * Creates a document fragment. 456 * 457 * @param {Document} doc Target document. 458 * @return {DocumentFragment} Resulting document fragment node. 459 */ 460 function domCreateDocumentFragment(doc) { 461 return doc.createDocumentFragment(); 462 } 463 464 /** 465 * Redirect to document.getElementById 466 * 467 * @param {Document} doc Target document. 468 * @param {String} id Id of requested node. 469 * @return {Element|Null} Resulting element. 470 */ 471 function domGetElementById(doc, id) { 472 return doc.getElementById(id); 473 } 474 475 /** 476 * Redirect to window.setInterval 477 * 478 * @param {Window} win Target window. 479 * @param {Function} fun Callback function. 480 * @param {Number} time Time in milliseconds. 481 * @return {Object} Contract id. 482 */ 483 function windowSetInterval(win, fun, time) { 484 return win.setInterval(fun, time); 485 } 486 487 /** 488 * Redirect to window.clearInterval 489 * 490 * @param {Window} win Target window. 491 * @param {object} id Contract id. 492 * @return {any} NOTE: Return type unknown? 493 */ 494 function windowClearInterval(win, id) { 495 return win.clearInterval(id); 496 } 497 498 /** 499 * Determines whether one node is recursively contained in another. 500 * @param parent The parent node. 501 * @param child The node to look for in parent. 502 * @return parent recursively contains child 503 */ 504 function containsNode(parent, child) { 505 while (parent != child && child.parentNode) { 506 child = child.parentNode; 507 } 508 return parent == child; 509 }; 510 /** 511 * @fileoverview This file contains javascript utility functions that 512 * do not depend on anything defined elsewhere. 513 * 514 */ 515 516 /** 517 * Returns the value of the length property of the given object. Used 518 * to reduce compiled code size. 519 * 520 * @param {Array | String} a The string or array to interrogate. 521 * @return {Number} The value of the length property. 522 */ 523 function jsLength(a) { 524 return a.length; 525 } 526 527 var min = Math.min; 528 var max = Math.max; 529 var ceil = Math.ceil; 530 var floor = Math.floor; 531 var round = Math.round; 532 var abs = Math.abs; 533 534 /** 535 * Copies all properties from second object to the first. Modifies to. 536 * 537 * @param {Object} to The target object. 538 * @param {Object} from The source object. 539 */ 540 function copyProperties(to, from) { 541 foreachin(from, function(p) { 542 to[p] = from[p]; 543 }); 544 } 545 546 /** 547 * Iterates over the array, calling the given function for each 548 * element. 549 * 550 * @param {Array} array 551 * @param {Function} fn 552 */ 553 function foreach(array, fn) { 554 var I = jsLength(array); 555 for (var i = 0; i < I; ++i) { 556 fn(array[i], i); 557 } 558 } 559 560 /** 561 * Safely iterates over all properties of the given object, calling 562 * the given function for each property. If opt_all isn't true, uses 563 * hasOwnProperty() to assure the property is on the object, not on 564 * its prototype. 565 * 566 * @param {Object} object 567 * @param {Function} fn 568 * @param {Boolean} opt_all If true, also iterates over inherited properties. 569 */ 570 function foreachin(object, fn, opt_all) { 571 for (var i in object) { 572 if (opt_all || !object.hasOwnProperty || object.hasOwnProperty(i)) { 573 fn(i, object[i]); 574 } 575 } 576 } 577 578 /** 579 * Appends the second array to the first, copying its elements. 580 * Optionally only a slice of the second array is copied. 581 * 582 * @param {Array} a1 Target array (modified). 583 * @param {Array} a2 Source array. 584 * @param {Number} opt_begin Begin of slice of second array (optional). 585 * @param {Number} opt_end End (exclusive) of slice of second array (optional). 586 */ 587 function arrayAppend(a1, a2, opt_begin, opt_end) { 588 var i0 = opt_begin || 0; 589 var i1 = opt_end || jsLength(a2); 590 for (var i = i0; i < i1; ++i) { 591 a1.push(a2[i]); 592 } 593 } 594 595 /** 596 * Trim whitespace from begin and end of string. 597 * 598 * @see testStringTrim(); 599 * 600 * @param {String} str Input string. 601 * @return {String} Trimmed string. 602 */ 603 function stringTrim(str) { 604 return stringTrimRight(stringTrimLeft(str)); 605 } 606 607 /** 608 * Trim whitespace from beginning of string. 609 * 610 * @see testStringTrimLeft(); 611 * 612 * @param {String} str Input string. 613 * @return {String} Trimmed string. 614 */ 615 function stringTrimLeft(str) { 616 return str.replace(/^\s+/, ""); 617 } 618 619 /** 620 * Trim whitespace from end of string. 621 * 622 * @see testStringTrimRight(); 623 * 624 * @param {String} str Input string. 625 * @return {String} Trimmed string. 626 */ 627 function stringTrimRight(str) { 628 return str.replace(/\s+$/, ""); 629 } 630 631 /** 632 * Jscompiler wrapper for parseInt() with base 10. 633 * 634 * @param {String} s String repersentation of a number. 635 * 636 * @return {Number} The integer contained in s, converted on base 10. 637 */ 638 function parseInt10(s) { 639 return parseInt(s, 10); 640 } 641 /** 642 * @fileoverview A simple formatter to project JavaScript data into 643 * HTML templates. The template is edited in place. I.e. in order to 644 * instantiate a template, clone it from the DOM first, and then 645 * process the cloned template. This allows for updating of templates: 646 * If the templates is processed again, changed values are merely 647 * updated. 648 * 649 * NOTE: IE DOM doesn't have importNode(). 650 * 651 * NOTE: The property name "length" must not be used in input 652 * data, see comment in jstSelect_(). 653 */ 654 655 656 /** 657 * Names of jstemplate attributes. These attributes are attached to 658 * normal HTML elements and bind expression context data to the HTML 659 * fragment that is used as template. 660 */ 661 var ATT_select = 'jsselect'; 662 var ATT_instance = 'jsinstance'; 663 var ATT_display = 'jsdisplay'; 664 var ATT_values = 'jsvalues'; 665 var ATT_eval = 'jseval'; 666 var ATT_transclude = 'transclude'; 667 var ATT_content = 'jscontent'; 668 669 670 /** 671 * Names of special variables defined by the jstemplate evaluation 672 * context. These can be used in js expression in jstemplate 673 * attributes. 674 */ 675 var VAR_index = '$index'; 676 var VAR_this = '$this'; 677 678 679 /** 680 * Context for processing a jstemplate. The context contains a context 681 * object, whose properties can be referred to in jstemplate 682 * expressions, and it holds the locally defined variables. 683 * 684 * @param {Object} opt_data The context object. Null if no context. 685 * 686 * @param {Object} opt_parent The parent context, from which local 687 * variables are inherited. Normally the context object of the parent 688 * context is the object whose property the parent object is. Null for the 689 * context of the root object. 690 * 691 * @constructor 692 */ 693 function JsExprContext(opt_data, opt_parent) { 694 var me = this; 695 696 /** 697 * The local context of the input data in which the jstemplate 698 * expressions are evaluated. Notice that this is usually an Object, 699 * but it can also be a scalar value (and then still the expression 700 * $this can be used to refer to it). Notice this can be a scalar 701 * value, including undefined. 702 * 703 * @type {Object} 704 */ 705 me.data_ = opt_data; 706 707 /** 708 * The context for variable definitions in which the jstemplate 709 * expressions are evaluated. Other than for the local context, 710 * which replaces the parent context, variable definitions of the 711 * parent are inherited. The special variable $this points to data_. 712 * 713 * @type {Object} 714 */ 715 me.vars_ = {}; 716 if (opt_parent) { 717 copyProperties(me.vars_, opt_parent.vars_); 718 } 719 this.vars_[VAR_this] = me.data_; 720 } 721 722 723 /** 724 * Evaluates the given expression in the context of the current 725 * context object and the current local variables. 726 * 727 * @param {String} expr A javascript expression. 728 * 729 * @param {Element} template DOM node of the template. 730 * 731 * @return The value of that expression. 732 */ 733 JsExprContext.prototype.jseval = function(expr, template) { 734 with (this.vars_) { 735 with (this.data_) { 736 try { 737 return (function() { 738 return eval('[' + expr + '][0]'); 739 }).call(template); 740 } catch (e) { 741 return null; 742 } 743 } 744 } 745 } 746 747 748 /** 749 * Clones the current context for a new context object. The cloned 750 * context has the data object as its context object and the current 751 * context as its parent context. It also sets the $index variable to 752 * the given value. This value usually is the position of the data 753 * object in a list for which a template is instantiated multiply. 754 * 755 * @param {Object} data The new context object. 756 * 757 * @param {Number} index Position of the new context when multiply 758 * instantiated. (See implementation of jstSelect().) 759 * 760 * @return {JsExprContext} 761 */ 762 JsExprContext.prototype.clone = function(data, index) { 763 var ret = new JsExprContext(data, this); 764 ret.setVariable(VAR_index, index); 765 if (this.resolver_) { 766 ret.setSubTemplateResolver(this.resolver_); 767 } 768 return ret; 769 } 770 771 772 /** 773 * Binds a local variable to the given value. If set from jstemplate 774 * jsvalue expressions, variable names must start with $, but in the 775 * API they only have to be valid javascript identifier. 776 * 777 * @param {String} name 778 * 779 * @param {Object} value 780 */ 781 JsExprContext.prototype.setVariable = function(name, value) { 782 this.vars_[name] = value; 783 } 784 785 786 /** 787 * Sets the function used to resolve the values of the transclude 788 * attribute into DOM nodes. By default, this is jstGetTemplate(). The 789 * value set here is inherited by clones of this context. 790 * 791 * @param {Function} resolver The function used to resolve transclude 792 * ids into a DOM node of a subtemplate. The DOM node returned by this 793 * function will be inserted into the template instance being 794 * processed. Thus, the resolver function must instantiate the 795 * subtemplate as necessary. 796 */ 797 JsExprContext.prototype.setSubTemplateResolver = function(resolver) { 798 this.resolver_ = resolver; 799 } 800 801 802 /** 803 * Resolves a sub template from an id. Used to process the transclude 804 * attribute. If a resolver function was set using 805 * setSubTemplateResolver(), it will be used, otherwise 806 * jstGetTemplate(). 807 * 808 * @param {String} id The id of the sub template. 809 * 810 * @return {Node} The root DOM node of the sub template, for direct 811 * insertion into the currently processed template instance. 812 */ 813 JsExprContext.prototype.getSubTemplate = function(id) { 814 return (this.resolver_ || jstGetTemplate).call(this, id); 815 } 816 817 818 /** 819 * HTML template processor. Data values are bound to HTML templates 820 * using the attributes transclude, jsselect, jsdisplay, jscontent, 821 * jsvalues. The template is modifed in place. The values of those 822 * attributes are JavaScript expressions that are evaluated in the 823 * context of the data object fragment. 824 * 825 * @param {JsExprContext} context Context created from the input data 826 * object. 827 * 828 * @param {Element} template DOM node of the template. This will be 829 * processed in place. After processing, it will still be a valid 830 * template that, if processed again with the same data, will remain 831 * unchanged. 832 */ 833 function jstProcess(context, template) { 834 var processor = new JstProcessor(); 835 processor.run_([ processor, processor.jstProcess_, context, template ]); 836 } 837 838 839 /** 840 * Internal class used by jstemplates to maintain context. 841 * NOTE: This is necessary to process deep templates in Safari 842 * which has a relatively shallow stack. 843 * @class 844 */ 845 function JstProcessor() { 846 } 847 848 849 /** 850 * Runs the state machine, beginning with function "start". 851 * 852 * @param {Array} start The first function to run, in the form 853 * [object, method, args ...] 854 */ 855 JstProcessor.prototype.run_ = function(start) { 856 var me = this; 857 858 me.queue_ = [ start ]; 859 while (jsLength(me.queue_)) { 860 var f = me.queue_.shift(); 861 f[1].apply(f[0], f.slice(2)); 862 } 863 } 864 865 866 /** 867 * Appends a function to be called later. 868 * Analogous to calling that function on a subsequent line, or a subsequent 869 * iteration of a loop. 870 * 871 * @param {Array} f A function in the form [object, method, args ...] 872 */ 873 JstProcessor.prototype.enqueue_ = function(f) { 874 this.queue_.push(f); 875 } 876 877 878 /** 879 * Implements internals of jstProcess. 880 * 881 * @param {JsExprContext} context 882 * 883 * @param {Element} template 884 */ 885 JstProcessor.prototype.jstProcess_ = function(context, template) { 886 var me = this; 887 888 var transclude = domGetAttribute(template, ATT_transclude); 889 if (transclude) { 890 var tr = context.getSubTemplate(transclude); 891 if (tr) { 892 domReplaceChild(tr, template); 893 me.enqueue_([ me, me.jstProcess_, context, tr ]); 894 } else { 895 domRemoveNode(template); 896 } 897 return; 898 } 899 900 var select = domGetAttribute(template, ATT_select); 901 if (select) { 902 me.jstSelect_(context, template, select); 903 return; 904 } 905 906 var display = domGetAttribute(template, ATT_display); 907 if (display) { 908 if (!context.jseval(display, template)) { 909 displayNone(template); 910 return; 911 } 912 913 displayDefault(template); 914 } 915 916 917 var values = domGetAttribute(template, ATT_values); 918 if (values) { 919 me.jstValues_(context, template, values); 920 } 921 922 var expressions = domGetAttribute(template, ATT_eval); 923 if (expressions) { 924 foreach(expressions.split(/\s*;\s*/), function(expression) { 925 expression = stringTrim(expression); 926 if (jsLength(expression)) { 927 context.jseval(expression, template); 928 } 929 }); 930 } 931 932 var content = domGetAttribute(template, ATT_content); 933 if (content) { 934 me.jstContent_(context, template, content); 935 936 } else { 937 var childnodes = []; 938 for (var i = 0; i < jsLength(template.childNodes); ++i) { 939 if (template.childNodes[i].nodeType == DOM_ELEMENT_NODE) { 940 me.enqueue_( 941 [ me, me.jstProcess_, context, template.childNodes[i] ]); 942 } 943 } 944 } 945 } 946 947 948 /** 949 * Implements the jsselect attribute: evalutes the value of the 950 * jsselect attribute in the current context, with the current 951 * variable bindings (see JsExprContext.jseval()). If the value is an 952 * array, the current template node is multiplied once for every 953 * element in the array, with the array element being the context 954 * object. If the array is empty, or the value is undefined, then the 955 * current template node is dropped. If the value is not an array, 956 * then it is just made the context object. 957 * 958 * @param {JsExprContext} context The current evaluation context. 959 * 960 * @param {Element} template The currently processed node of the template. 961 * 962 * @param {String} select The javascript expression to evaluate. 963 * 964 * @param {Function} process The function to continue processing with. 965 */ 966 JstProcessor.prototype.jstSelect_ = function(context, template, select) { 967 var me = this; 968 969 var value = context.jseval(select, template); 970 domRemoveAttribute(template, ATT_select); 971 972 var instance = domGetAttribute(template, ATT_instance); 973 var instance_last = false; 974 if (instance) { 975 if (instance.charAt(0) == '*') { 976 instance = parseInt10(instance.substr(1)); 977 instance_last = true; 978 } else { 979 instance = parseInt10(instance); 980 } 981 } 982 983 var multiple = (value !== null && 984 typeof value == 'object' && 985 typeof value.length == 'number'); 986 var multiple_empty = (multiple && value.length == 0); 987 988 if (multiple) { 989 if (multiple_empty) { 990 if (!instance) { 991 domSetAttribute(template, ATT_select, select); 992 domSetAttribute(template, ATT_instance, '*0'); 993 displayNone(template); 994 } else { 995 domRemoveNode(template); 996 } 997 998 } else { 999 displayDefault(template); 1000 if (instance === null || instance === "" || instance === undefined || 1001 (instance_last && instance < jsLength(value) - 1)) { 1002 var templatenodes = []; 1003 var instances_start = instance || 0; 1004 for (var i = instances_start + 1; i < jsLength(value); ++i) { 1005 var node = domCloneNode(template); 1006 templatenodes.push(node); 1007 domInsertBefore(node, template); 1008 } 1009 templatenodes.push(template); 1010 1011 for (var i = 0; i < jsLength(templatenodes); ++i) { 1012 var ii = i + instances_start; 1013 var v = value[ii]; 1014 var t = templatenodes[i]; 1015 1016 me.enqueue_([ me, me.jstProcess_, context.clone(v, ii), t ]); 1017 var instanceStr = (ii == jsLength(value) - 1 ? '*' : '') + ii; 1018 me.enqueue_( 1019 [ null, postProcessMultiple_, t, select, instanceStr ]); 1020 } 1021 1022 } else if (instance < jsLength(value)) { 1023 var v = value[instance]; 1024 1025 me.enqueue_( 1026 [me, me.jstProcess_, context.clone(v, instance), template]); 1027 var instanceStr = (instance == jsLength(value) - 1 ? '*' : '') 1028 + instance; 1029 me.enqueue_( 1030 [ null, postProcessMultiple_, template, select, instanceStr ]); 1031 } else { 1032 domRemoveNode(template); 1033 } 1034 } 1035 } else { 1036 if (value == null) { 1037 domSetAttribute(template, ATT_select, select); 1038 displayNone(template); 1039 } else { 1040 me.enqueue_( 1041 [ me, me.jstProcess_, context.clone(value, 0), template ]); 1042 me.enqueue_( 1043 [ null, postProcessSingle_, template, select ]); 1044 } 1045 } 1046 } 1047 1048 1049 /** 1050 * Sets ATT_select and ATT_instance following recursion to jstProcess. 1051 * 1052 * @param {Element} template The template 1053 * 1054 * @param {String} select The jsselect string 1055 * 1056 * @param {String} instanceStr The new value for the jsinstance attribute 1057 */ 1058 function postProcessMultiple_(template, select, instanceStr) { 1059 domSetAttribute(template, ATT_select, select); 1060 domSetAttribute(template, ATT_instance, instanceStr); 1061 } 1062 1063 1064 /** 1065 * Sets ATT_select and makes the element visible following recursion to 1066 * jstProcess. 1067 * 1068 * @param {Element} template The template 1069 * 1070 * @param {String} select The jsselect string 1071 */ 1072 function postProcessSingle_(template, select) { 1073 domSetAttribute(template, ATT_select, select); 1074 displayDefault(template); 1075 } 1076 1077 1078 /** 1079 * Implements the jsvalues attribute: evaluates each of the values and 1080 * assigns them to variables in the current context (if the name 1081 * starts with '$', javascript properties of the current template node 1082 * (if the name starts with '.'), or DOM attributes of the current 1083 * template node (otherwise). Since DOM attribute values are always 1084 * strings, the value is coerced to string in the latter case, 1085 * otherwise it's the uncoerced javascript value. 1086 * 1087 * @param {JsExprContext} context Current evaluation context. 1088 * 1089 * @param {Element} template Currently processed template node. 1090 * 1091 * @param {String} valuesStr Value of the jsvalues attribute to be 1092 * processed. 1093 */ 1094 JstProcessor.prototype.jstValues_ = function(context, template, valuesStr) { 1095 var values = valuesStr.split(/\s*;\s*/); 1096 for (var i = 0; i < jsLength(values); ++i) { 1097 var colon = values[i].indexOf(':'); 1098 if (colon < 0) { 1099 continue; 1100 } 1101 var label = stringTrim(values[i].substr(0, colon)); 1102 var value = context.jseval(values[i].substr(colon + 1), template); 1103 1104 if (label.charAt(0) == '$') { 1105 context.setVariable(label, value); 1106 1107 } else if (label.charAt(0) == '.') { 1108 var nameSpaceLabel = label.substr(1).split('.'); 1109 var nameSpaceObject = template; 1110 var nameSpaceDepth = jsLength(nameSpaceLabel); 1111 for (var j = 0, J = nameSpaceDepth - 1; j < J; ++j) { 1112 var jLabel = nameSpaceLabel[j]; 1113 if (!nameSpaceObject[jLabel]) { 1114 nameSpaceObject[jLabel] = {}; 1115 } 1116 nameSpaceObject = nameSpaceObject[jLabel]; 1117 } 1118 nameSpaceObject[nameSpaceLabel[nameSpaceDepth - 1]] = value; 1119 } else if (label) { 1120 if (typeof value == 'boolean') { 1121 if (value) { 1122 domSetAttribute(template, label, label); 1123 } else { 1124 domRemoveAttribute(template, label); 1125 } 1126 } else { 1127 domSetAttribute(template, label, '' + value); 1128 } 1129 } 1130 } 1131 } 1132 1133 1134 /** 1135 * Implements the jscontent attribute. Evalutes the expression in 1136 * jscontent in the current context and with the current variables, 1137 * and assigns its string value to the content of the current template 1138 * node. 1139 * 1140 * @param {JsExprContext} context Current evaluation context. 1141 * 1142 * @param {Element} template Currently processed template node. 1143 * 1144 * @param {String} content Value of the jscontent attribute to be 1145 * processed. 1146 */ 1147 JstProcessor.prototype.jstContent_ = function(context, template, content) { 1148 var value = '' + context.jseval(content, template); 1149 if (template.innerHTML == value) { 1150 return; 1151 } 1152 while (template.firstChild) { 1153 domRemoveNode(template.firstChild); 1154 } 1155 var t = domCreateTextNode(ownerDocument(template), value); 1156 domAppendChild(template, t); 1157 } 1158 1159 1160 /** 1161 * Helps to implement the transclude attribute, and is the initial 1162 * call to get hold of a template from its ID. 1163 * 1164 * @param {String} name The ID of the HTML element used as template. 1165 * 1166 * @returns {Element} The DOM node of the template. (Only element 1167 * nodes can be found by ID, hence it's a Element.) 1168 */ 1169 function jstGetTemplate(name) { 1170 var section = domGetElementById(document, name); 1171 if (section) { 1172 var ret = domCloneNode(section); 1173 domRemoveAttribute(ret, 'id'); 1174 return ret; 1175 } else { 1176 return null; 1177 } 1178 } 1179 1180 window['jstGetTemplate'] = jstGetTemplate; 1181 window['jstProcess'] = jstProcess; 1182 window['JsExprContext'] = JsExprContext; 1183