1 // ========================================================================== 2 // Project: SproutCore - JavaScript Application Framework 3 // Copyright: ©2006-2011 Strobe Inc. and contributors. 4 // Portions ©2008-2011 Apple Inc. All rights reserved. 5 // License: Licensed under MIT license (see license.js) 6 // ========================================================================== 7 8 sc_require('system/builder'); 9 10 /** set update mode on context to replace content (preferred) */ 11 SC.MODE_REPLACE = 'replace'; 12 13 /** set update mode on context to append content */ 14 SC.MODE_APPEND = 'append'; 15 16 /** set update mode on context to prepend content */ 17 SC.MODE_PREPEND = 'prepend'; 18 19 /** list of numeric properties that should not have 'px' appended */ 20 SC.NON_PIXEL_PROPERTIES = ['zIndex', 'opacity']; 21 22 /** a list of styles that get expanded into multiple properties, add more as you discover them */ 23 SC.COMBO_STYLES = { 24 WebkitTransition: ['WebkitTransitionProperty', 'WebkitTransitionDuration', 'WebkitTransitionDelay', 'WebkitTransitionTimingFunction'] 25 }; 26 27 /** 28 @namespace 29 30 A RenderContext is a builder that can be used to generate HTML for views or 31 to update an existing element. Rather than making changes to an element 32 directly, you use a RenderContext to queue up changes to the element, 33 finally applying those changes or rendering the new element when you are 34 finished. 35 36 You will not usually create a render context yourself but you will be passed 37 a render context as the first parameter of your render() method on custom 38 views. 39 40 Render contexts are essentially arrays of strings. You can add a string to 41 the context by calling push(). You can retrieve the entire array as a 42 single string using join(). This is basically the way the context is used 43 for views. You are passed a render context and expected to add strings of 44 HTML to the context like a normal array. Later, the context will be joined 45 into a single string and converted into real HTML for display on screen. 46 47 In addition to the core push and join methods, the render context also 48 supports some extra methods that make it easy to build tags. 49 50 context.begin() <-- begins a new tag context 51 context.end() <-- ends the tag context... 52 */ 53 SC.RenderContext = SC.Builder.create( 54 /** @lends SC.RenderContext */ { 55 56 SELF_CLOSING: SC.CoreSet.create().addEach(['area', 'base', 'basefront', 'br', 'hr', 'input', 'img', 'link', 'meta']), 57 58 /** 59 When you create a context you should pass either a tag name or an element 60 that should be used as the basis for building the context. If you pass 61 an element, then the element will be inspected for class names, styles 62 and other attributes. You can also call update() or replace() to 63 modify the element with you context contents. 64 65 If you do not pass any parameters, then we assume the tag name is 'div'. 66 67 A second parameter, parentContext, is used internally for chaining. You 68 should never pass a second argument. 69 70 @param {String|DOMElement} tagNameOrElement 71 @returns {SC.RenderContext} receiver 72 */ 73 init: function (tagNameOrElement, prevContext) { 74 var tagNameOrElementIsString; 75 76 // if a prevContext was passed, setup with that first... 77 if (prevContext) { 78 this.prevObject = prevContext; 79 this.strings = prevContext.strings; 80 this.offset = prevContext.length + prevContext.offset; 81 } 82 83 if (!this.strings) this.strings = []; 84 85 // if tagName is string, just setup for rendering new tagName 86 if (tagNameOrElement === undefined) { 87 tagNameOrElement = 'div'; 88 tagNameOrElementIsString = YES; 89 } 90 else if (tagNameOrElement === 'div' || tagNameOrElement === 'label' || tagNameOrElement === 'a') { 91 // Fast path for common tags. 92 tagNameOrElementIsString = YES; 93 } 94 else if (SC.typeOf(tagNameOrElement) === SC.T_STRING) { 95 tagNameOrElement = tagNameOrElement.toLowerCase(); 96 tagNameOrElementIsString = YES; 97 } 98 99 if (tagNameOrElementIsString) { 100 this._tagName = tagNameOrElement; 101 this._needsTag = YES; // used to determine if end() needs to wrap tag 102 this.needsContent = YES; 103 104 // increase length of all contexts to leave space for opening tag 105 var c = this; 106 while (c) { c.length++; c = c.prevObject; } 107 108 this.strings.push(null); 109 this._selfClosing = this.SELF_CLOSING.contains(tagNameOrElement); 110 } else { 111 this._elem = tagNameOrElement; 112 this._needsTag = NO; 113 this.length = 0; 114 this.needsContent = NO; 115 } 116 return this; 117 }, 118 119 // .......................................................... 120 // PROPERTIES 121 // 122 123 // NOTE: We store this as an actual array of strings so that browsers that 124 // support dense arrays will use them. 125 /** 126 The current working array of strings. 127 128 @type Array 129 */ 130 strings: null, 131 132 /** 133 this initial offset into the strings array where this context instance 134 has its opening tag. 135 136 @type Number 137 */ 138 offset: 0, 139 140 /** 141 the current number of strings owned by the context, including the opening 142 tag. 143 144 @type Number 145 */ 146 length: 0, 147 148 /** 149 Specify the method that should be used to update content on the element. 150 In almost all cases you want to replace the content. Very carefully 151 managed code (such as in CollectionView) can append or prepend content 152 instead. 153 154 You probably do not want to change this property unless you know what you 155 are doing. 156 157 @type String 158 */ 159 updateMode: SC.MODE_REPLACE, 160 161 /** 162 YES if the context needs its content filled in, not just its outer 163 attributes edited. This will be set to YES anytime you push strings into 164 the context or if you don't create it with an element to start with. 165 */ 166 needsContent: NO, 167 168 // .......................................................... 169 // CORE STRING API 170 // 171 172 /** 173 Returns the string at the designated index. If you do not pass anything 174 returns the string array. This index is an offset from the start of the 175 strings owned by this context. 176 177 @param {Number} idx the index 178 @returns {String|Array} 179 */ 180 get: function (idx) { 181 var strings = this.strings || []; 182 return (idx === undefined) ? strings.slice(this.offset, this.length) : strings[idx + this.offset]; 183 }, 184 185 /** @deprecated */ 186 html: function (line) { 187 //@if(debug) 188 SC.warn("Developer Warning: SC.RenderContext:html() is no longer used to push HTML strings. Please use `push()` instead."); 189 //@endif 190 return this.push(line); 191 }, 192 193 /** 194 Adds a string to the render context for later joining and insertion. To 195 HTML escape the string, see the similar text() method instead. 196 197 Note: You can pass multiple string arguments to this method and each will 198 be pushed. 199 200 When used in render() for example, 201 202 MyApp.MyView = SC.View.extend({ 203 204 innerText: '', 205 206 render: function (context) { 207 var innerText = this.get('innerText'); 208 209 // This will be pushed into the DOM all at once. 210 context.push('<div class="inner-div">', innerText, '<span class="inner-span">**</span></div>'); 211 } 212 213 }); 214 215 @param {String} line the HTML to add to the context 216 @returns {SC.RenderContext} receiver 217 */ 218 push: function (line) { 219 var strings = this.strings, len = arguments.length; 220 if (!strings) this.strings = strings = []; // create array lazily 221 222 if (len > 1) { 223 strings.push.apply(strings, arguments); 224 } else { 225 strings.push(line); 226 } 227 228 // adjust string length for context and all parents... 229 var c = this; 230 while (c) { c.length += len; c = c.prevObject; } 231 232 this.needsContent = YES; 233 234 return this; 235 }, 236 237 /** 238 Pushes the passed string to the render context for later joining and 239 insertion, but first escapes the string to ensure that no user-entered HTML 240 is processed as HTML. To push the string without escaping, see the similar 241 push() method instead. 242 243 Note: You can pass multiple string arguments to this method and each will 244 be escaped and pushed. 245 246 When used in render() for example, 247 248 MyApp.MyView = SC.View.extend({ 249 250 userText: '<script src="http://maliciousscripts.com"></script>', 251 252 render: function (context) { 253 var userText = this.get('userText'); 254 255 // Pushes "<script src="http://maliciousscripts.com"></script>" in the DOM 256 context.text(userText); 257 } 258 259 }); 260 261 @param {String} line the text to add to the context 262 @returns {SC.RenderContext} receiver 263 */ 264 text: function () { 265 var len = arguments.length, 266 idx = 0; 267 268 for (idx = 0; idx < len; idx++) { 269 this.push(SC.RenderContext.escapeHTML(arguments[idx])); 270 } 271 272 return this; 273 }, 274 275 /** 276 Joins the strings together, closes any open tags and returns the final result. 277 278 @param {String} joinChar optional string to use in joins. def empty string 279 @returns {String} joined string 280 */ 281 join: function (joinChar) { 282 // generate tag if needed... 283 if (this._needsTag) this.end(); 284 285 var strings = this.strings; 286 return strings ? strings.join(joinChar || '') : ''; 287 }, 288 289 // .......................................................... 290 // GENERATING 291 // 292 293 /** 294 Begins a new render context based on the passed tagName or element. 295 Generate said context using end(). 296 297 @returns {SC.RenderContext} new context 298 */ 299 begin: function (tagNameOrElement) { 300 return SC.RenderContext(tagNameOrElement, this); 301 }, 302 303 /** 304 If the current context targets an element, this method returns the 305 element. If the context does not target an element, this method will 306 render the context into an offscreen element and return it. 307 308 @returns {DOMElement} the element 309 */ 310 element: function () { 311 return this._elem ? this._elem : SC.$(this.join())[0]; 312 }, 313 314 /** 315 Removes an element with the passed id in the currently managed element. 316 */ 317 remove: function (elementId) { 318 if (!elementId) return; 319 320 var el, elem = this._elem; 321 if (!elem || !elem.removeChild) return; 322 323 el = document.getElementById(elementId); 324 if (el) { 325 el = elem.removeChild(el); 326 el = null; 327 } 328 }, 329 330 /** 331 If an element was set on this context when it was created, this method 332 will actually apply any changes to the element itself. If you have not 333 written any inner html into the context, then the innerHTML of the 334 element will not be changed, otherwise it will be replaced with the new 335 innerHTML. 336 337 Also, any attributes, id, classNames or styles you've set will be 338 updated as well. This also ends the editing context session and cleans 339 up. 340 341 @returns {SC.RenderContext} previous context or null if top 342 */ 343 update: function () { 344 var elem = this._elem, 345 mode = this.updateMode, 346 cq, value, factory, cur, next; 347 348 // this._innerHTMLReplaced = NO; 349 350 if (!elem) { 351 // throw new Error("Cannot update context because there is no source element"); 352 return; 353 } 354 355 cq = this.$(); 356 357 // replace innerHTML 358 if (this.length > 0) { 359 // this._innerHTMLReplaced = YES; 360 if (mode === SC.MODE_REPLACE) { 361 cq.html(this.join()); 362 } else { 363 factory = elem.cloneNode(false); 364 factory.innerHTML = this.join(); 365 cur = factory.firstChild; 366 while (cur) { 367 next = cur.nextSibling; 368 elem.insertBefore(cur, next); 369 cur = next; 370 } 371 cur = next = factory = null; // cleanup 372 } 373 } 374 375 // attributes, styles, and class names will already have been set. 376 377 // id="foo" 378 if (this._idDidChange && (value = this._id)) { 379 cq.attr('id', value); 380 } 381 382 // now cleanup element... 383 elem = this._elem = null; 384 return this.prevObject || this; 385 }, 386 387 // these are temporary objects are reused by end() to avoid memory allocs. 388 _DEFAULT_ATTRS: {}, 389 390 /** 391 Ends the current tag editing context. This will generate the tag string 392 including any attributes you might have set along with a closing tag. 393 394 The generated HTML will be added to the render context strings. This will 395 also return the previous context if there is one or the receiver. 396 397 If you do not have a current tag, this does nothing. 398 399 @returns {SC.RenderContext} 400 */ 401 end: function () { 402 // NOTE: If you modify this method, be careful to consider memory usage 403 // and performance here. This method is called frequently during renders 404 // and we want it to be as fast as possible. 405 406 // generate opening tag. 407 408 // get attributes first. Copy in className + styles... 409 var tag = '', styleStr = '', key, value, 410 attrs = this._attrs, className = this._classes, 411 id = this._id, styles = this._styles, strings, selfClosing; 412 413 // add tag to tag array 414 tag = '<' + this._tagName; 415 416 // add any attributes... 417 if (attrs || className || styles || id) { 418 if (!attrs) attrs = this._DEFAULT_ATTRS; 419 if (id) attrs.id = id; 420 // old versions of safari (5.0)!!!! throw an error if we access 421 // attrs.class. meh... 422 if (className) attrs['class'] = className.join(' '); 423 424 // add in styles. note how we avoid memory allocs here to keep things 425 // fast... 426 if (styles) { 427 for (key in styles) { 428 if (!styles.hasOwnProperty(key)) continue; 429 value = styles[key]; 430 if (value === null) continue; // skip empty styles 431 if (typeof value === SC.T_NUMBER && !SC.NON_PIXEL_PROPERTIES.contains(key)) value += "px"; 432 styleStr = styleStr + this._dasherizeStyleName(key) + ": " + value + "; "; 433 } 434 attrs.style = styleStr; 435 } 436 437 // now convert attrs hash to tag array... 438 tag = tag + ' '; // add space for joining0 439 for (key in attrs) { 440 if (!attrs.hasOwnProperty(key)) continue; 441 value = attrs[key]; 442 if (value === null) continue; // skip empty attrs 443 tag = tag + key + '="' + value + '" '; 444 } 445 446 // if we are using the DEFAULT_ATTRS temporary object, make sure we 447 // reset. 448 if (attrs === this._DEFAULT_ATTRS) { 449 delete attrs.style; 450 delete attrs['class']; 451 delete attrs.id; 452 } 453 454 } 455 456 // this is self closing if there is no content in between and selfClosing 457 // is not set to false. 458 strings = this.strings; 459 selfClosing = (this._selfClosing === NO) ? NO : (this.length === 1); 460 tag = tag + (selfClosing ? ' />' : '>'); 461 462 strings[this.offset] = tag; 463 464 // now generate closing tag if needed... 465 if (!selfClosing) { 466 strings.push('</' + this._tagName + '>'); 467 468 // increase length of receiver and all parents 469 var c = this; 470 while (c) { c.length++; c = c.prevObject; } 471 } 472 473 // if there was a source element, cleanup to avoid memory leaks 474 this._elem = null; 475 return this.prevObject || this; 476 }, 477 478 /** 479 Generates a tag with the passed options. Like calling context.begin().end(). 480 481 @param {String} tagName optional tag name. default 'div' 482 @param {Hash} opts optional tag options. defaults to empty options. 483 @returns {SC.RenderContext} receiver 484 */ 485 tag: function (tagName, opts) { 486 return this.begin(tagName, opts).end(); 487 }, 488 489 // .......................................................... 490 // BASIC HELPERS 491 // 492 493 /** 494 Reads outer tagName if no param is passed, sets tagName otherwise. 495 496 @param {String} tagName pass to set tag name. 497 @returns {String|SC.RenderContext} tag name or receiver 498 */ 499 tagName: function (tagName) { 500 if (tagName === undefined) { 501 if (!this._tagName && this._elem) this._tagName = this._elem.tagName; 502 return this._tagName; 503 } else { 504 this._tagName = tagName; 505 this._tagNameDidChange = YES; 506 return this; 507 } 508 }, 509 510 /** 511 Reads the outer tag id if no param is passed, sets the id otherwise. 512 513 @param {String} idName the id or set 514 @returns {String|SC.RenderContext} id or receiver 515 */ 516 id: function (idName) { 517 if (idName === undefined) { 518 if (!this._id && this._elem) this._id = this._elem.id; 519 return this._id; 520 } else { 521 this._id = idName; 522 this._idDidChange = YES; 523 return this; 524 } 525 }, 526 527 // .......................................................... 528 // CSS CLASS NAMES SUPPORT 529 // 530 531 /** @deprecated */ 532 classNames: function (deprecatedArg) { 533 if (deprecatedArg) { 534 //@if(debug) 535 SC.warn("Developer Warning: SC.RenderContext:classNames() (renamed to classes()) is no longer used to set classes, only to retrieve them. Please use `setClass()` instead."); 536 //@endif 537 return this.setClass(deprecatedArg); 538 } else { 539 //@if(debug) 540 SC.warn("Developer Warning: SC.RenderContext:classNames() has been renamed to classes() to better match the API of setClass() and resetClasses(). Please use `classes()` instead."); 541 //@endif 542 return this.classes(); 543 } 544 }, 545 546 /** 547 Retrieves the class names for the current context. 548 549 @returns {Array} classNames array 550 */ 551 classes: function () { 552 if (!this._classes) { 553 if (this._elem) { 554 // Get the classes from the element. 555 var attr = this.$().attr('class'); 556 557 if (attr && (attr = attr.toString()).length > 0) { 558 this._classes = attr.split(/\s/); 559 } else { 560 // No class on the element. 561 this._classes = []; 562 } 563 } else { 564 this._classes = []; 565 } 566 } 567 568 return this._classes; 569 }, 570 571 /** 572 Adds a class or classes to the current context. 573 574 This is a convenience method that simply calls setClass(nameOrClasses, YES). 575 576 @param {String|Array} nameOrClasses a class name or an array of class names 577 @returns {SC.RenderContext} receiver 578 */ 579 addClass: function (nameOrClasses) { 580 // Convert arrays into objects for use by setClass 581 if (SC.typeOf(nameOrClasses) === SC.T_ARRAY) { 582 for (var i = 0, length = nameOrClasses.length, obj = {}; i < length; i++) { 583 obj[nameOrClasses[i]] = YES; 584 } 585 nameOrClasses = obj; 586 } 587 588 return this.setClass(nameOrClasses, YES); 589 }, 590 591 /** 592 Removes the specified class name from the current context. 593 594 This is a convenience method that simply calls setClass(name, NO). 595 596 @param {String} name the class to remove 597 @returns {SC.RenderContext} receiver 598 */ 599 removeClass: function (name) { 600 return this.setClass(name, NO); 601 }, 602 603 /** 604 Sets or unsets class names on the current context. 605 606 You can either pass a single class name and a boolean indicating whether 607 the value should be added or removed, or you can pass a hash with all 608 the class names you want to add or remove with a boolean indicating 609 whether they should be there or not. 610 611 When used in render() for example, 612 613 MyApp.MyView = SC.View.extend({ 614 615 isAdministrator: NO, 616 617 render: function (context) { 618 var isAdministrator = this.get('isAdministrator'); 619 620 // Sets the 'is-admin' class appropriately. 621 context.setClass('is-admin', isAdministrator); 622 } 623 624 }); 625 626 @param {String|Hash} nameOrClasses either a single class name or a hash of class names with boolean values indicating whether to add or remove the class 627 @param {Boolean} shouldAdd if a single class name for nameOrClasses is passed, this 628 @returns {SC.RenderContext} receiver 629 */ 630 setClass: function (nameOrClasses, shouldAdd) { 631 var didChange = NO, 632 classes = this.classes(); 633 634 // Add the updated classes to the internal classes object. 635 if (SC.typeOf(nameOrClasses) === SC.T_ARRAY) { 636 //@if(debug) 637 SC.warn("Developer Warning: SC.RenderContext:setClass() should not be passed an array of class names. To remain compatible with calls to the deprecated classNames() function, all classes on the current context will be replaced with the given array, but it would be more accurate in the future to call resetClasses() and addClass() or setClass(hash) instead. Please update your code accordingly."); 638 //@endif 639 this.resetClasses(); 640 classes = this.classes(); 641 642 for (var i = 0, length = nameOrClasses.length; i < length; i++) { 643 didChange = this._setClass(classes, nameOrClasses[i], YES) || didChange; 644 } 645 } else if (SC.typeOf(nameOrClasses) === SC.T_HASH) { 646 for (var name in nameOrClasses) { 647 if (!nameOrClasses.hasOwnProperty(name)) continue; 648 649 shouldAdd = nameOrClasses[name]; 650 didChange = this._setClass(classes, name, shouldAdd) || didChange; 651 } 652 } else { 653 didChange = this._setClass(classes, nameOrClasses, shouldAdd); 654 } 655 656 if (didChange) { 657 this._classesDidChange = YES; 658 659 // Apply the styles to the element if we have one already. 660 if (this._elem) { 661 this.$().attr('class', classes.join(' ')); 662 } 663 } 664 665 return this; 666 }, 667 668 /** @private */ 669 _setClass: function (classes, name, shouldAdd) { 670 var didChange = NO, 671 idx; 672 673 idx = classes.indexOf(name); 674 if (idx >= 0 && !shouldAdd) { 675 classes.splice(idx, 1); 676 didChange = YES; 677 } else if (idx < 0 && shouldAdd) { 678 classes.push(name); 679 didChange = YES; 680 } 681 682 return didChange; 683 }, 684 685 /** 686 Returns YES if the outer tag current has the passed class name, NO 687 otherwise. 688 689 @param {String} name the class name 690 @returns {Boolean} 691 */ 692 hasClass: function (name) { 693 if (this._elem) { 694 return this.$().hasClass(name); 695 } 696 697 return this.classes().indexOf(name) >= 0; 698 }, 699 700 /** @deprecated */ 701 resetClassNames: function () { 702 //@if(debug) 703 SC.warn("Developer Warning: SC.RenderContext:resetClassNames() has been renamed to resetClasses to better match the API of classes(GET) and setClass(SET). Please use `resetClasses()` instead."); 704 //@endif 705 return this.resetClasses(); 706 }, 707 708 /** 709 Removes all class names from the context. 710 711 Be aware that setClass() only effects the class names specified. If there 712 are existing class names that are not modified by a call to setClass(), they 713 will remain on the context. For example, if you call addClass('a') and 714 addClass('b') followed by setClass({ b:NO }), the 'b' class will be 715 removed, but the 'a' class will be unaffected. 716 717 If you want to call setClass() or addClass() to replace all classes, you 718 should call this method first. 719 720 @returns {SC.RenderContext} receiver 721 */ 722 resetClasses: function () { 723 var didChange = NO, 724 classes = this.classes(); 725 726 // Check for changes. 727 didChange = classes.length; 728 729 // Reset. 730 this._classes = []; 731 if (didChange) { 732 this._classesDidChange = YES; 733 734 // Apply the styles to the element if we have one already. 735 if (this._elem) { 736 this.$().attr('class', ''); 737 } 738 } 739 740 return this; 741 }, 742 743 // .......................................................... 744 // CSS Styles Support 745 // 746 747 /** @private */ 748 _STYLE_REGEX: /-?\s*([^:\s]+)\s*:\s*([^;]+)\s*;?/g, 749 750 /** 751 Retrieves the current styles for the context. 752 753 @returns {Object} styles hash 754 */ 755 styles: function (deprecatedArg) { 756 // Fast path! 757 if (deprecatedArg) { 758 //@if(debug) 759 SC.warn("Developer Warning: SC.RenderContext:styles() is no longer used to set styles, only to retrieve them. Please use `setStyle(%@)` instead.".fmt(deprecatedArg)); 760 //@endif 761 return this.setStyle(deprecatedArg); 762 } 763 764 if (!this._styles) { 765 if (this._elem) { 766 // Get the styles from the element. 767 var attr = this.$().attr('style'); 768 769 if (attr && (attr = attr.toString()).length > 0) { 770 // Ensure attributes are lower case for IE 771 if (SC.browser.name === SC.BROWSER.ie) { 772 attr = attr.toLowerCase(); 773 } 774 var styles = {}, 775 match, 776 regex = this._STYLE_REGEX; 777 778 regex.lastIndex = 0; 779 while (match = regex.exec(attr)) { 780 styles[this._camelizeStyleName(match[1])] = match[2]; 781 } 782 783 this._styles = styles; 784 } else { 785 // No style on the element. 786 this._styles = {}; 787 } 788 } else { 789 this._styles = {}; 790 } 791 } 792 793 return this._styles; 794 }, 795 796 /** 797 Adds the specified style to the current context. 798 799 This is a convenience method that simply calls setStyle(nameOrStyles, value). 800 801 @param {String|Object} nameOrStyles the name of a style or a hash of style names with values 802 @param {String|Number} value style value if a single style name for nameOrStyles is passed 803 @returns {SC.RenderContext} receiver 804 */ 805 addStyle: function (nameOrStyles, value) { 806 //@if(debug) 807 // Notify when this function isn't being used properly (in debug mode only). 808 /*jshint eqnull:true*/ 809 if (SC.typeOf(nameOrStyles) === SC.T_STRING && value == null) { 810 SC.warn("Developer Warning: SC.RenderContext:addStyle is not meant to be used to remove attributes by setting the value to null or undefined. It would be more correct to use setStyle(%@, %@).".fmt(nameOrStyles, value)); 811 } 812 //@endif 813 return this.setStyle(nameOrStyles, value); 814 }, 815 816 /** 817 Removes the specified style from the current context. 818 819 This is a convenience method that simply calls setStyle(name, undefined). 820 821 @param {String} styleName the name of the style to remove 822 @returns {SC.RenderContext} receiver 823 */ 824 removeStyle: function (styleName) { 825 return this.setStyle(styleName); 826 }, 827 828 /** @deprecated */ 829 css: function (nameOrStyles, value) { 830 //@if(debug) 831 SC.warn("Developer Warning: In order to simplify the API to a few core functions, SC.RenderContext:css() has been deprecated in favor of setStyle which performs the same function. Please use `setStyle(%@, %@)` instead.".fmt(nameOrStyles, value)); 832 //@endif 833 return this.setStyle(nameOrStyles, value); 834 }, 835 836 /** 837 Sets or unsets a style or styles on the context. 838 839 Passing a value will set the value for the given style name, passing a null 840 or undefined value will unset any current value for the given style name and 841 remove it. 842 843 Be aware that setStyle() only effects the styles specified. If there 844 are existing styles that are not modified by a call to setStyle(), they 845 will remain on the context. For example, if you call addStyle('margin-left', 10) 846 and addStyle('margin-right', 10) followed by setClass({ 'margin-right': null }), 847 the 'margin-right' style will be removed, but the 'margin-left' style will 848 be unaffected. 849 850 If you want to call setStyle() or addStyle() to replace all styles, you 851 should call resetStyles() method first. 852 853 When used in render() for example, 854 855 MyApp.MyView = SC.View.extend({ 856 857 textColor: 'blue', 858 859 // By default this syle will not appear since the value is null. 860 fontFamily: null, 861 862 render: function (context) { 863 var textColor = this.get('textColor'), 864 fontFamily = this.get('fontFamily'); 865 866 // Set the `color` and `fontFamily` styles. 867 context.setStyle({ 868 color: textColor, 869 fontFamily: fontFamily 870 }); 871 } 872 }); 873 874 @param {String|Object} nameOrStyles the name of a style or a hash of style names with values 875 @param {String|Number} [value] style value if a single style name for nameOrStyles is passed 876 @returns {SC.RenderContext} receiver 877 */ 878 setStyle: function (nameOrStyles, value) { 879 var didChange = NO, 880 styles = this.styles(); 881 882 // Add the updated styles to the internal styles object. 883 if (SC.typeOf(nameOrStyles) === SC.T_HASH) { 884 for (var key in nameOrStyles) { 885 // Call a separate function so that it may be optimized. 886 didChange = this._sc_setStyleFromObject(didChange, key, nameOrStyles, styles); 887 } 888 } else { 889 didChange = this._deleteComboStyles(styles, nameOrStyles); 890 didChange = this._setOnHash(styles, nameOrStyles, value) || didChange; 891 } 892 893 // Set the styles on the element if we have one already. 894 if (didChange && this._elem) { 895 // Note: jQuery .css doesn't remove old styles 896 this.$().css(styles); 897 } 898 899 return this; 900 }, 901 902 /** @private Sets the style by key from the styles object. This allows for optimization outside of the for..in loop. */ 903 _sc_setStyleFromObject: function (didChange, key, stylesObject, styles) { 904 if (!stylesObject.hasOwnProperty(key)) return false; 905 906 var value = stylesObject[key]; 907 908 didChange = this._deleteComboStyles(styles, key) || didChange; 909 didChange = this._setOnHash(styles, key, value) || didChange; 910 911 return didChange; 912 }, 913 914 /** @private */ 915 _deleteComboStyles: function (styles, key) { 916 var comboStyles = SC.COMBO_STYLES[key], 917 didChange = NO, tmp; 918 919 if (comboStyles) { 920 for (var idx = 0, idxLen = comboStyles.length; idx < idxLen; idx++) { 921 tmp = comboStyles[idx]; 922 if (styles[tmp]) { 923 delete styles[tmp]; 924 didChange = YES; 925 } 926 } 927 } 928 929 return didChange; 930 }, 931 932 /** @private Sets or unsets the key:value on the hash and returns whether a change occurred. */ 933 _setOnHash: function (hash, key, value) { 934 var cur = hash[key], 935 didChange = true; 936 937 /*jshint eqnull:true */ 938 if (cur == null && value != null) { 939 hash[key] = value; 940 } else if (cur != null && value == null) { 941 // Unset using '' so that jQuery will remove the value, null is not reliable (ex. WebkitTransform) 942 hash[key] = ''; 943 } else if (cur != value) { 944 hash[key] = value; 945 } else { 946 didChange = false; 947 } 948 949 return didChange; 950 }, 951 952 /** 953 Removes all styles from the context. 954 955 Be aware that setStyle() only affects the styles specified. If there 956 are existing styles that are not modified by a call to setStyle(), they 957 will remain on the context. For example, if you call addStyle('margin-left', 10) 958 and addStyle('margin-right', 10) followed by setClass({ 'margin-right': null }), 959 the 'margin-right' style will be removed, but the 'margin-left' style will 960 be unaffected. 961 962 If you want to call setStyle() or addStyle() to replace all styles, you 963 should call this method first. 964 965 @returns {SC.RenderContext} receiver 966 */ 967 resetStyles: function () { 968 var didChange = NO, 969 styles = this.styles(); 970 971 // Check for changes (i.e. are there any properties in the object). 972 for (var key in styles) { 973 if (!styles.hasOwnProperty(key)) continue; 974 975 didChange = YES; 976 } 977 978 // Reset. 979 this._styles = {}; 980 if (didChange) { 981 // Apply the styles to the element if we have one already. 982 if (this._elem) { 983 this.$().attr('style', ''); 984 } 985 } 986 987 return this; 988 }, 989 990 // .......................................................... 991 // ARBITRARY ATTRIBUTES SUPPORT 992 // 993 994 /** 995 Retrieves the current attributes for the context, less the class and style 996 attributes. 997 998 If you retrieve the attributes hash to edit it, you must pass the hash back 999 to setAttr in order for it to be applied to the element on rendering. 1000 1001 Use classes() or styles() to get those specific attributes. 1002 1003 @returns {Object} attributes hash 1004 */ 1005 attrs: function () { 1006 if (!this._attrs) { 1007 if (this._elem) { 1008 // Get the attributes from the element. 1009 var attrs = {}, 1010 elAttrs = this._elem.attributes, 1011 length = elAttrs.length; 1012 1013 for (var i = 0, attr, name; i < length; i++) { 1014 attr = elAttrs.item(i); 1015 name = attr.nodeName; 1016 if (name.match(/^(?!class|style).*$/i)) { 1017 attrs[name] = attr.value; 1018 } 1019 } 1020 1021 this._attrs = attrs; 1022 } else { 1023 this._attrs = {}; 1024 } 1025 } 1026 1027 return this._attrs; 1028 }, 1029 1030 /** @deprecated */ 1031 attr: function (nameOrAttrs, value) { 1032 // Fast path. 1033 if (nameOrAttrs) { 1034 1035 if (SC.typeOf(nameOrAttrs) === SC.T_HASH || value !== undefined) { 1036 //@if(debug) 1037 SC.warn("Developer Warning: SC.RenderContext:attr() is no longer used to set attributes. Please use `setAttr()` instead, which matches the API of setClass() and setStyle()."); 1038 //@endif 1039 return this.setAttr(nameOrAttrs, value); 1040 } else { 1041 //@if(debug) 1042 SC.warn("Developer Warning: SC.RenderContext:attr() is no longer used to get an attribute. Please use `attrs()` instead to retrieve the hash and check properties on it directly, which matches the API of classes() and styles()."); 1043 //@endif 1044 return this.attrs()[nameOrAttrs]; 1045 } 1046 } 1047 //@if(debug) 1048 SC.warn("Developer Warning: SC.RenderContext:attr() is no longer used to get attributes. Please use `attrs()` instead, which matches the API of classes() and styles()."); 1049 //@endif 1050 1051 return this.attrs(); 1052 }, 1053 1054 /** 1055 Adds the specified attribute to the current context. 1056 1057 This is a convenience method that simply calls setAttr(nameOrAttrs, value). 1058 1059 @param {String|Object} nameOrAttrs the name of an attribute or a hash of attribute names with values 1060 @param {String|Number} value attribute value if a single attribute name for nameOrAttrs is passed 1061 @returns {SC.RenderContext} receiver 1062 */ 1063 addAttr: function (nameOrAttrs, value) { 1064 //@if(debug) 1065 // Notify when this function isn't being used properly (in debug mode only). 1066 /*jshint eqnull:true*/ 1067 if (SC.typeOf(nameOrAttrs) === SC.T_STRING && value == null) { 1068 SC.warn("Developer Warning: SC.RenderContext:addAttr is not meant to be used to remove attributes by setting the value to null or undefined. It would be more correct to use setAttr(%@, %@).".fmt(nameOrAttrs, value)); 1069 } 1070 //@endif 1071 return this.setAttr(nameOrAttrs, value); 1072 }, 1073 1074 /** 1075 Removes the specified attribute from the current context. 1076 1077 This is a convenience method that simply calls setAttr(name, undefined). 1078 1079 @param {String} styleName the name of the attribute to remove 1080 @returns {SC.RenderContext} receiver 1081 */ 1082 removeAttr: function (name) { 1083 //@if(debug) 1084 // Notify when this function isn't being used properly (in debug mode only). 1085 if (name.match(/^(class|style)$/i)) { 1086 SC.error("Developer Error: SC.RenderContext:removeAttr is not meant to be used to remove the style or class attribute. You should use resetClasses() or resetStyles()."); 1087 } 1088 //@endif 1089 1090 return this.setAttr(name); 1091 }, 1092 1093 /** 1094 Sets or unsets an attribute or attributes on the context. Passing a value 1095 will set the value for the given attribute name, passing a null or undefined 1096 value will unset any current value for the given attribute name and remove 1097 it. 1098 1099 When used in render() for example, 1100 1101 MyApp.MyView = SC.View.extend({ 1102 1103 // By default this syle will not appear since the value is null. 1104 title: null, 1105 1106 render: function (context) { 1107 var title = this.get('title'); 1108 1109 // Set the `title` and `data-test` attributes. 1110 context.setAttr({ 1111 title: title, 1112 'data-test': SC.buildMode === 'test' 1113 }); 1114 } 1115 }); 1116 1117 @param {String|Object} nameOrAttrs the name of an attribute or a hash of attribute names with values 1118 @param {String} [value] attribute value if a single attribute name for nameOrAttrs is passed 1119 @returns {SC.RenderContext} receiver 1120 */ 1121 setAttr: function (nameOrAttrs, value) { 1122 var didChange = NO, 1123 attrs = this.attrs(), 1124 key; 1125 1126 //@if(debug) 1127 // Add some developer support to prevent improper use (in debug mode only). 1128 var foundImproperUse = NO; 1129 if (SC.typeOf(nameOrAttrs) === SC.T_HASH) { 1130 1131 for (key in nameOrAttrs) { 1132 if (key.match(/^(class|style)$/i)) { 1133 foundImproperUse = YES; 1134 } 1135 } 1136 } else if (nameOrAttrs.match(/^(class|style)$/i)) { 1137 foundImproperUse = YES; 1138 } 1139 1140 if (foundImproperUse) { 1141 SC.error("Developer Error: setAttr() is not meant to set class or style attributes. Only classes and styles added with their relevant methods will be used. Please use setClass() or setStyle()."); 1142 } 1143 //@endif 1144 1145 // Add the updated attrs to the internal attrs object. 1146 if (SC.typeOf(nameOrAttrs) === SC.T_HASH) { 1147 for (key in nameOrAttrs) { 1148 if (!nameOrAttrs.hasOwnProperty(key)) continue; 1149 1150 value = nameOrAttrs[key]; 1151 didChange = this._setOnHash(attrs, key, value) || didChange; 1152 } 1153 } else { 1154 didChange = this._setOnHash(attrs, nameOrAttrs, value); 1155 } 1156 1157 if (didChange) { 1158 this._attrsDidChange = YES; 1159 1160 // Apply the attrs to the element if we have one already. 1161 if (this._elem) { 1162 this.$().attr(nameOrAttrs, value); 1163 } 1164 } 1165 1166 return this; 1167 }, 1168 1169 // 1170 // COREQUERY SUPPORT 1171 // 1172 /** 1173 Returns a CoreQuery instance for the element this context wraps (if 1174 it wraps any). If a selector is passed, the CoreQuery instance will 1175 be for nodes matching that selector. 1176 1177 Renderers may use this to modify DOM. 1178 */ 1179 $: function (sel) { 1180 var ret, elem = this._elem; 1181 ret = !elem ? SC.$([]) : (sel === undefined) ? SC.$(elem) : SC.$(sel, elem); 1182 elem = null; 1183 return ret; 1184 }, 1185 1186 1187 /** @private 1188 */ 1189 _camelizeStyleName: function (name) { 1190 // IE wants the first letter lowercase so we can allow normal behavior 1191 var needsCap = name.match(/^-(webkit|moz|o)-/), 1192 camelized = SC.String.camelize(name); 1193 1194 if (needsCap) { 1195 return camelized.substr(0, 1).toUpperCase() + camelized.substr(1); 1196 } else { 1197 return camelized; 1198 } 1199 }, 1200 1201 /** @private 1202 Converts camelCased style names to dasherized forms 1203 */ 1204 _dasherizeStyleName: function (name) { 1205 var dasherized = SC.String.dasherize(name); 1206 if (dasherized.match(/^(webkit|moz|ms|o)-/)) { dasherized = '-' + dasherized; } 1207 return dasherized; 1208 } 1209 1210 }); 1211 1212 (function () { 1213 // this regex matches all <, > or &, unless & is immediately followed by at last 1 up to 7 alphanumeric 1214 // characters and a ;. For instance: 1215 // Some evil <script src="evil.js"> but this is legal & these are not & &illegalese; 1216 // would become: 1217 // Some evil <script src="evil.js"> but this is legal & these are not & &illegalese; 1218 var _escapeHTMLRegex = /[<>]|&(?![\d\w#]{1,7};)/g, _escapeHTMLMethod = function (match) { 1219 switch (match) { 1220 case '&': 1221 return '&'; 1222 case '<': 1223 return '<'; 1224 case '>': 1225 return '>'; 1226 } 1227 }; 1228 1229 /** 1230 Helper method escapes the passed string to ensure HTML is displayed as 1231 plain text while preserving HTML entities like ' , à, etc. 1232 You should make sure you pass all user-entered data through 1233 this method to avoid errors. You can also do this with the text() helper 1234 method on a render context. 1235 1236 @param {String|Number} text value to escape 1237 @returns {String} string with all HTML values properly escaped 1238 */ 1239 SC.RenderContext.escapeHTML = function (text) { 1240 if (!text) return ''; 1241 if (SC.typeOf(text) === SC.T_NUMBER) { text = text.toString(); } 1242 return text.replace(_escapeHTMLRegex, _escapeHTMLMethod); 1243 }; 1244 })(); 1245