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