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 
  9 /**
 10   @static
 11   @constant
 12 */
 13 SC.LIST_ITEM_ACTION_CANCEL = 'sc-list-item-cancel-action';
 14 
 15 /**
 16   @static
 17   @constant
 18 */
 19 SC.LIST_ITEM_ACTION_REFRESH = 'sc-list-item-cancel-refresh';
 20 
 21 /**
 22   @static
 23   @constant
 24 */
 25 SC.LIST_ITEM_ACTION_EJECT = 'sc-list-item-cancel-eject';
 26 
 27 /**
 28   @class
 29 
 30   Many times list items need to display a lot more than just a label of text.
 31   You often need to include checkboxes, icons, right icons, extra counts and
 32   an action or warning icon to the far right.
 33 
 34   A ListItemView can implement all of this for you in a more efficient way
 35   than you might get if you simply put together a list item on your own using
 36   views.
 37 
 38   @extends SC.View
 39   @extends SC.Control
 40   @extends SC.InlineEditable
 41   @since SproutCore 1.0
 42 */
 43 SC.ListItemView = SC.View.extend(SC.InlineEditable, SC.Control,
 44 /** @scope SC.ListItemView.prototype */ {
 45 
 46   /**
 47     @type Array
 48     @default ['sc-list-item-view']
 49     @see SC.View#classNames
 50   */
 51   classNames: ['sc-list-item-view'],
 52 
 53   /**
 54     @type Array
 55     @default ['disclosureState', 'escapeHTML']
 56     @see SC.View#displayProperties
 57   */
 58   displayProperties: ['disclosureState', 'escapeHTML', 'isDropTarget'],
 59 
 60 
 61   // ..........................................................
 62   // KEY PROPERTIES
 63   //
 64 
 65   /**
 66     The index of the content object in the ListView to which this
 67     ListItemView belongs.
 68 
 69     For example, if this ListItemView represents the first object
 70     in a ListView, this property would be 0.
 71 
 72     @type Number
 73     @default null
 74     @readOnly
 75   */
 76   contentIndex: null,
 77 
 78   /**
 79     (displayDelegate) True if you want the item view to display an icon.
 80 
 81     If false, the icon on the list item view will be hidden.  Otherwise,
 82     space will be left for the icon next to the list item view.
 83 
 84     @type Boolean
 85     @default NO
 86   */
 87   hasContentIcon: NO,
 88 
 89   /**
 90     (displayDelegate) True if you want the item view to display a right icon.
 91 
 92     If false, the icon on the list item view will be hidden.  Otherwise,
 93     space will be left for the icon next to the list item view.
 94 
 95     @type Boolean
 96     @default NO
 97   */
 98   hasContentRightIcon: NO,
 99 
100   /**
101     (displayDelegate) True if you want space to be allocated for a branch
102     arrow.
103 
104     If false, the space for the branch arrow will be collapsed.
105 
106     @type Boolean
107     @default NO
108   */
109   hasContentBranch: NO,
110 
111   /**
112     (displayDelegate) The name of the property used for the checkbox value.
113 
114     The checkbox will only be visible if this key is not null.
115 
116     @type String
117     @default null
118   */
119   contentCheckboxKey: null,
120 
121   /**
122     The URL or CSS class name to use for the icon. This is only used if
123     contentIconKey is null, or returns null from the delegate.
124 
125     @type String
126     @default null
127   */
128   icon: null,
129 
130   /**
131     Whether this item is the drop target of a drag operation.
132 
133     If the list view supports the SC.DROP_ON operation, it will set this
134     property on whichever list item view is the current target of the drop.
135 
136     When true, the 'drop-target' class is added to the element.
137 
138     @type Boolean
139     @default false
140   */
141   isDropTarget: NO,
142 
143   /**
144     (displayDelegate) Property key to use for the icon url
145 
146     This property will be checked on the content object to determine the
147     icon to display.  It must return either a URL or a CSS class name.
148 
149     @type String
150     @default NO
151   */
152   contentIconKey: null,
153 
154   /**
155     The URL or CSS class name to use for the right icon. This is only used if
156     contentRightIconKey is null, or returns null from the delegate.
157 
158     @type String
159     @default null
160   */
161   rightIcon: null,
162 
163   /**
164     (displayDelegate) Property key to use for the right icon url
165 
166     This property will be checked on the content object to determine the
167     icon to display.  It must return either a URL or a CSS class name.
168 
169     @type String
170     @default null
171   */
172   contentRightIconKey: null,
173 
174   /**
175     (displayDelegate) The name of the property used for label itself
176 
177     If null, then the content object itself will be used..
178 
179     @type String
180     @default null
181   */
182   contentValueKey: null,
183 
184   /**
185     IF true, the label value will be escaped to avoid HTML injection attacks.
186     You should only disable this option if you are sure you will only
187     display content that is already escaped and you need the added
188     performance gain.
189 
190     @type Boolean
191     @default YES
192   */
193   escapeHTML: YES,
194 
195   /**
196     (displayDelegate) The name of the property used to find the count of
197     unread items.
198 
199     The count will only be visible if this property is not null and the
200     returned value is not 0.
201 
202     @type String
203     @default null
204   */
205   contentUnreadCountKey: null,
206 
207   /**
208     (displayDelegate) The name of the property used to determine if the item
209     is a branch or leaf (i.e. if the branch icon should be displayed to the
210     right edge.)
211 
212     If this is null, then the branch view will be completely hidden.
213     Otherwise space will be allocated for it.
214 
215     @type String
216     @default null
217   */
218   contentIsBranchKey: null,
219 
220   /**
221     Indent to use when rendering a list item with an outline level > 0.  The
222     left edge of the list item will be indented by this amount for each
223     outline level.
224 
225     @type Number
226     @default 16
227   */
228   outlineIndent: 16,
229 
230   /**
231     Outline level for this list item.  Usually set by the collection view.
232 
233     @type Number
234     @default 0
235   */
236   outlineLevel: 0,
237 
238   /**
239     Disclosure state for this list item.  Usually set by the collection view
240     when the list item is created. Possible values:
241 
242       - SC.LEAF_NODE
243       - SC.BRANCH_OPEN
244       - SC.BRANCH_CLOSED
245 
246     @type String
247     @default SC.LEAF_NODE
248   */
249   disclosureState: SC.LEAF_NODE,
250 
251   /**
252     The validator to use for the inline text field created when the list item
253     is edited.
254   */
255   validator: null,
256 
257   contentKeys: {
258     contentValueKey: 'title',
259     contentCheckboxKey: 'checkbox',
260     contentIconKey:  'icon',
261     contentRightIconKey: 'rightIcon',
262     contentUnreadCountKey: 'count',
263     contentIsBranchKey: 'branch'
264   },
265 
266   /** @private */
267   contentPropertyDidChange: function () {
268     //if (this.get('isEditing')) this.discardEditing();
269     if (this.get('contentIsEditable') !== this.contentIsEditable()) {
270       this.notifyPropertyChange('contentIsEditable');
271     }
272 
273     this.displayDidChange();
274   },
275 
276   /**
277     Determines if content is editable or not. Checkboxes and other related
278     components will render disabled if an item is not editable.
279 
280     @field
281     @type Boolean
282     @observes content
283   */
284   contentIsEditable: function () {
285     var content = this.get('content');
286     return content && (content.get ? content.get('isEditable') !== NO : NO);
287   }.property('content').cacheable(),
288 
289   /**
290     @type Object
291     @default SC.InlineTextFieldDelegate
292   */
293   inlineEditorDelegate: SC.InlineTextFieldDelegate,
294 
295   /**
296     Finds and retrieves the element containing the label.  This is used
297     for inline editing.  The default implementation returns a CoreQuery
298     selecting any label elements.   If you override renderLabel() you
299     probably need to override this as well.
300 
301     @returns {jQuery} jQuery object selecting label elements
302   */
303   $label: function () {
304     return this.$('label');
305   },
306 
307   /** @private
308     Determines if the event occurred inside an element with the specified
309     classname or not.
310   */
311   _isInsideElementWithClassName: function (className, evt) {
312     var layer = this.get('layer');
313     if (!layer) return NO; // no layer yet -- nothing to do
314 
315     var el = SC.$(evt.target);
316     var ret = NO;
317     while (!ret && el.length > 0 && (el[0] !== layer)) {
318       if (el.hasClass(className)) ret = YES;
319       el = el.parent();
320     }
321     el = layer = null; //avoid memory leaks
322     return ret;
323   },
324 
325   /** @private
326     Returns YES if the list item has a checkbox and the event occurred
327     inside of it.
328   */
329   _isInsideCheckbox: function (evt) {
330     var del = this.displayDelegate;
331     var checkboxKey = this.getDelegateProperty('contentCheckboxKey', del);
332     return checkboxKey && this._isInsideElementWithClassName('sc-checkbox-view', evt);
333   },
334 
335   /** @private
336     Returns YES if the list item has a disclosure triangle and the event
337     occurred inside of it.
338   */
339   _isInsideDisclosure: function (evt) {
340     if (this.get('disclosureState') === SC.LEAF_NODE) return NO;
341     return this._isInsideElementWithClassName('sc-disclosure-view', evt);
342   },
343 
344   /** @private
345     Returns YES if the list item has a right icon and the event
346     occurred inside of it.
347   */
348   _isInsideRightIcon: function (evt) {
349     var del = this.displayDelegate;
350     var rightIconKey = this.getDelegateProperty('hasContentRightIcon', del) || !SC.none(this.rightIcon);
351     return rightIconKey && this._isInsideElementWithClassName('right-icon', evt);
352   },
353 
354   /** @private
355     mouseDown is handled only for clicks on the checkbox view or or action
356     button.
357   */
358   mouseDown: function (evt) {
359     // Fast path, reject secondary clicks.
360     if (evt.which && evt.which !== 1) return false;
361 
362     // if content is not editable, then always let collection view handle the
363     // event.
364     if (!this.get('contentIsEditable')) return NO;
365 
366     // if occurred inside checkbox, item view should handle the event.
367     if (this._isInsideCheckbox(evt)) {
368       this._addCheckboxActiveState();
369       this._isMouseDownOnCheckbox = YES;
370       this._isMouseInsideCheckbox = YES;
371       return YES; // listItem should handle this event
372 
373     } else if (this._isInsideDisclosure(evt)) {
374       this._addDisclosureActiveState();
375       this._isMouseDownOnDisclosure = YES;
376       this._isMouseInsideDisclosure = YES;
377       return YES;
378     } else if (this._isInsideRightIcon(evt)) {
379       this._addRightIconActiveState();
380       this._isMouseDownOnRightIcon = YES;
381       this._isMouseInsideRightIcon = YES;
382       return YES;
383     }
384 
385     return NO; // let the collection view handle this event
386   },
387 
388   /** @private */
389   mouseUp: function (evt) {
390     var ret = NO, del, checkboxKey, content, state, idx, set;
391 
392     // if mouse was down in checkbox -- then handle mouse up, otherwise
393     // allow parent view to handle event.
394     if (this._isMouseDownOnCheckbox) {
395 
396       // update only if mouse inside on mouse up...
397       if (this._isInsideCheckbox(evt)) {
398         del = this.displayDelegate;
399         checkboxKey = this.getDelegateProperty('contentCheckboxKey', del);
400         content = this.get('content');
401         if (content && content.get) {
402           var value = content.get(checkboxKey);
403           value = (value === SC.MIXED_STATE) ? YES : !value;
404           content.set(checkboxKey, value); // update content
405           this.displayDidChange(); // repaint view...
406         }
407       }
408 
409       this._removeCheckboxActiveState();
410       ret = YES;
411 
412     // if mouse as down on disclosure -- handle mouse up.  otherwise pass on
413     // to parent.
414     } else if (this._isMouseDownOnDisclosure) {
415       if (this._isInsideDisclosure(evt)) {
416         state = this.get('disclosureState');
417         idx   = this.get('contentIndex');
418         set   = (!SC.none(idx)) ? SC.IndexSet.create(idx) : null;
419         del = this.get('displayDelegate');
420 
421         if (state === SC.BRANCH_OPEN) {
422           if (set && del && del.collapse) del.collapse(set);
423           else this.set('disclosureState', SC.BRANCH_CLOSED);
424           this.displayDidChange();
425 
426         } else if (state === SC.BRANCH_CLOSED) {
427           if (set && del && del.expand) del.expand(set);
428           else this.set('disclosureState', SC.BRANCH_OPEN);
429           this.displayDidChange();
430         }
431       }
432 
433       this._removeDisclosureActiveState();
434       ret = YES;
435     // if mouse was down in right icon -- then handle mouse up, otherwise
436     // allow parent view to handle event.
437     } else if (this._isMouseDownOnRightIcon) {
438       this._removeRightIconActiveState();
439       ret = YES;
440     }
441 
442     // clear cached info
443     this._isMouseInsideCheckbox = this._isMouseDownOnCheckbox = NO;
444     this._isMouseDownOnDisclosure = this._isMouseInsideDisclosure = NO;
445     this._isMouseInsideRightIcon = this._isMouseDownOnRightIcon = NO;
446     return ret;
447   },
448 
449   /** @private */
450   mouseMoved: function (evt) {
451     if (this._isMouseDownOnCheckbox && this._isInsideCheckbox(evt)) {
452       this._addCheckboxActiveState();
453       this._isMouseInsideCheckbox = YES;
454     } else if (this._isMouseDownOnCheckbox) {
455       this._removeCheckboxActiveState();
456       this._isMouseInsideCheckbox = NO;
457     } else if (this._isMouseDownOnDisclosure && this._isInsideDisclosure(evt)) {
458       this._addDisclosureActiveState();
459       this._isMouseInsideDisclosure = YES;
460     } else if (this._isMouseDownOnDisclosure) {
461       this._removeDisclosureActiveState();
462       this._isMouseInsideDisclosure = NO;
463     } else if (this._isMouseDownOnRightIcon && this._isInsideRightIcon(evt)) {
464       this._addRightIconActiveState();
465       this._isMouseInsideRightIcon = YES;
466     } else if (this._isMouseDownOnRightIcon) {
467       this._removeRightIconActiveState();
468       this._isMouseInsideRightIcon = NO;
469     }
470 
471     return NO;
472   },
473 
474   /** @private */
475   touchStart: function (evt) {
476     return this.mouseDown(evt);
477   },
478 
479   /** @private */
480   touchEnd: function (evt) {
481     return this.mouseUp(evt);
482   },
483 
484   /** @private */
485   touchEntered: function (evt) {
486     return this.mouseEntered(evt);
487   },
488 
489   /** @private */
490   touchExited: function (evt) {
491     return this.mouseExited(evt);
492   },
493 
494 
495   /** @private */
496   _addCheckboxActiveState: function () {
497     if (this.get('isEnabled')) {
498       if (this._checkboxRenderDelegate) {
499         var source = this._checkboxRenderSource;
500 
501         source.set('isActive', YES);
502 
503         this._checkboxRenderDelegate.update(source, this.$('.sc-checkbox-view'));
504       } else {
505         // for backwards-compatibility.
506         this.$('.sc-checkbox-view').addClass('active');
507       }
508     }
509   },
510 
511   /** @private */
512   _removeCheckboxActiveState: function () {
513     if (this._checkboxRenderer) {
514       var source = this._checkboxRenderSource;
515 
516       source.set('isActive', NO);
517 
518       this._checkboxRenderDelegate.update(source, this.$('.sc-checkbox-view'));
519     } else {
520       // for backwards-compatibility.
521       this.$('.sc-checkbox-view').removeClass('active');
522     }
523   },
524 
525   /** @private */
526   _addDisclosureActiveState: function () {
527     if (this.get('isEnabled')) {
528       if (this._disclosureRenderDelegate) {
529         var source = this._disclosureRenderSource;
530         source.set('isActive', YES);
531 
532         this._disclosureRenderDelegate.update(source, this.$('.sc-disclosure-view'));
533       } else {
534         // for backwards-compatibility.
535         this.$('.sc-disclosure-view').addClass('active');
536       }
537 
538     }
539   },
540 
541   /** @private */
542   _removeDisclosureActiveState: function () {
543     if (this._disclosureRenderer) {
544       var source = this._disclosureRenderSource;
545       source.set('isActive', NO);
546 
547       this._disclosureRenderDelegate.update(source, this.$('.sc-disclosure-view'));
548     } else {
549       // for backwards-compatibility.
550       this.$('.sc-disclosure-view').addClass('active');
551     }
552   },
553 
554   /** @private */
555   _addRightIconActiveState: function () {
556     this.$('img.right-icon').setClass('active', YES);
557   },
558 
559   /** @private */
560   _removeRightIconActiveState: function () {
561     this.$('img.right-icon').removeClass('active');
562 
563     var pane = this.get('pane'),
564         del = this.displayDelegate,
565         target = this.getDelegateProperty('rightIconTarget', del),
566         action = this.getDelegateProperty('rightIconAction', del);
567 
568     if (action && pane) {
569        pane.rootResponder.sendAction(action, target, this, pane);
570     }
571 
572   },
573 
574   /** @private
575     Returns true if a click is on the label text itself to enable editing.
576 
577     Note that if you override renderLabel(), you probably need to override
578     this as well, or just $label() if you only want to control the element
579     returned.
580 
581     @param evt {Event} the mouseUp event.
582     @returns {Boolean} YES if the mouse was on the content element itself.
583   */
584   contentHitTest: function (evt) {
585     // if not content value is returned, not much to do.
586     var del = this.displayDelegate;
587     var labelKey = this.getDelegateProperty('contentValueKey', del);
588     if (!labelKey) return NO;
589 
590     // get the element to check for.
591     var el = this.$label()[0];
592     if (!el) return NO; // no label to check for.
593 
594     var cur = evt.target, layer = this.get('layer');
595     while (cur && (cur !== layer) && (cur !== window)) {
596       if (cur === el) return YES;
597       cur = cur.parentNode;
598     }
599 
600     return NO;
601   },
602 
603   /*
604     Edits the label portion of the list item. If scrollIfNeeded is YES, will
605     scroll to the item before editing it.
606 
607     @params {Boolean} if the parent scroll view should be scrolled to this item
608       before editing begins
609     @returns {Boolean} YES if successful
610   */
611   beginEditing: function (original, scrollIfNeeded) {
612     var el        = this.$label(),
613         parent    = this.get('parentView');
614 
615     // if possible, find a nearby scroll view and scroll into view.
616     // HACK: if we scrolled, then wait for a loop and get the item view again
617     // and begin editing.  Right now collection view will regenerate the item
618     // view too often.
619     if (scrollIfNeeded && this.scrollToVisible()) {
620       var collectionView = this.get('owner'),
621         idx = this.get('contentIndex');
622 
623       this.invokeLast(function () {
624         var item = collectionView.itemViewForContentIndex(idx);
625         if (item && item.beginEditing) item.beginEditing(NO);
626       });
627       return YES; // let the scroll happen then begin editing...
628     }
629 
630     else if (!parent || !el || el.get('length') === 0) return NO;
631 
632     else return original();
633   }.enhance(),
634 
635   /*
636     Configures the editor to overlay the label properly.
637   */
638   inlineEditorWillBeginEditing: function (editor, editable, value) {
639     var content   = this.get('content'),
640         del       = this.get('displayDelegate'),
641         labelKey  = this.getDelegateProperty('contentValueKey', del),
642         el        = this.$label(),
643         validator = this.get('validator'),
644         f, v, offset, fontSize, top, lineHeight, escapeHTML,
645         lineHeightShift, targetLineHeight;
646 
647     v = (labelKey && content) ? (content.get ? content.get(labelKey) : content[labelKey]) : content;
648 
649     f = this.computeFrameWithParentFrame(null);
650 
651     // if the label has a large line height, try to adjust it to something
652     // more reasonable so that it looks right when we show the popup editor.
653     lineHeight = this._oldLineHeight = el.css('lineHeight');
654     fontSize = el.css('fontSize');
655     top = this.$().css('top');
656 
657     if (top) top = parseInt(top.substring(0, top.length - 2), 0);
658     else top = 0;
659 
660     lineHeightShift = 0;
661 
662     if (fontSize && lineHeight) {
663       targetLineHeight = fontSize * 1.5;
664       if (targetLineHeight < lineHeight) {
665         el.css({ lineHeight: '1.5' });
666         lineHeightShift = (lineHeight - targetLineHeight) / 2;
667       } else this._oldLineHeight = null;
668     }
669 
670     el = el[0];
671     offset = SC.offset(el);
672 
673     f.x = offset.x;
674     f.y = offset.y + top + lineHeightShift;
675     f.height = el.offsetHeight;
676     f.width = el.offsetWidth;
677 
678     escapeHTML = this.get('escapeHTML');
679 
680     editor.set({
681       value: v,
682       exampleFrame: f,
683       exampleElement: el,
684       multiline: NO,
685       validator: validator,
686       escapeHTML: escapeHTML
687     });
688   },
689 
690   /** @private
691     Allow editing.
692   */
693   inlineEditorShouldBeginEditing: function (inlineEditor) {
694     return YES;
695   },
696 
697   /** @private
698    Hide the label view while the inline editor covers it.
699   */
700   inlineEditorDidBeginEditing: function (original, inlineEditor, value, editable) {
701     original(inlineEditor, value, editable);
702 
703     var el = this.$label();
704     this._oldOpacity = el.css('opacity');
705     el.css('opacity', 0.0);
706 
707     // restore old line height for original item if the old line height
708     // was saved.
709     if (this._oldLineHeight) el.css({ lineHeight: this._oldLineHeight });
710   }.enhance(),
711 
712   /** @private
713    Update the field value and make it visible again.
714   */
715   inlineEditorDidCommitEditing: function (editor, finalValue, editable) {
716     var content = this.get('content');
717     var del = this.displayDelegate;
718     var labelKey = this.getDelegateProperty('contentValueKey', del);
719 
720     if (labelKey && content) {
721       if (content.set) content.set(labelKey, finalValue);
722       else content[labelKey] = finalValue;
723     }
724 
725     else this.set('content', finalValue);
726 
727     this.displayDidChange();
728 
729     this._endEditing();
730   },
731 
732   _endEditing: function (original) {
733     this.$label().css('opacity', this._oldOpacity);
734 
735     original();
736   }.enhance(),
737 
738   /** SC.ListItemView is not able to update itself in place at this time. */
739   // TODO: add update: support.
740   isReusable: false,
741 
742   /** @private
743     Fills the passed html-array with strings that can be joined to form the
744     innerHTML of the receiver element.  Also populates an array of classNames
745     to set on the outer element.
746 
747     @param {SC.RenderContext} context
748     @param {Boolean} firstTime
749     @returns {void}
750   */
751   render: function (context, firstTime) {
752     var content = this.get('content'),
753         del     = this.displayDelegate,
754         level   = this.get('outlineLevel'),
755         indent  = this.get('outlineIndent'),
756         key, value, working, classArray = [];
757 
758     // add alternating row classes
759     classArray.push((this.get('contentIndex') % 2 === 0) ? 'even' : 'odd');
760     context.setClass('disabled', !this.get('isEnabled'));
761     context.setClass('drop-target', this.get('isDropTarget'));
762 
763     // outline level wrapper
764     working = context.begin("div").addClass("sc-outline");
765     if (level >= 0 && indent > 0) working.addStyle("left", indent * (level + 1));
766 
767     // handle disclosure triangle
768     value = this.get('disclosureState');
769     if (value !== SC.LEAF_NODE) {
770       this.renderDisclosure(working, value);
771       classArray.push('has-disclosure');
772     } else if (this._disclosureRenderSource) {
773       // If previously rendered a disclosure, clean up.
774       context.removeClass('has-disclosure');
775       this._disclosureRenderSource.destroy();
776 
777       this._disclosureRenderSource = this._disclosureRenderDelegate = null;
778     }
779 
780     // handle checkbox
781     key = this.getDelegateProperty('contentCheckboxKey', del);
782     if (key) {
783       value = content ? (content.get ? content.get(key) : content[key]) : NO;
784       if (value !== null) {
785         this.renderCheckbox(working, value);
786         classArray.push('has-checkbox');
787       } else if (this._checkboxRenderSource) {
788         // If previously rendered a checkbox, clean up.
789         context.removeClass('has-checkbox');
790         this._checkboxRenderSource.destroy();
791 
792         this._checkboxRenderSource = this._checkboxRenderDelegate = null;
793       }
794     }
795 
796     // handle icon
797     if (this.getDelegateProperty('hasContentIcon', del)) {
798       key = this.getDelegateProperty('contentIconKey', del);
799       value = (key && content) ? (content.get ? content.get(key) : content[key]) : null;
800 
801       if (value) {
802         this.renderIcon(working, value);
803         classArray.push('has-icon');
804       }
805     } else if (this.get('icon')) {
806       value = this.get('icon');
807       this.renderIcon(working, value);
808       classArray.push('has-icon');
809     }
810 
811     // handle label -- always invoke
812     key = this.getDelegateProperty('contentValueKey', del);
813     value = (key && content) ? (content.get ? content.get(key) : content[key]) : content;
814     if (value && SC.typeOf(value) !== SC.T_STRING) value = value.toString();
815     if (this.get('escapeHTML')) value = SC.RenderContext.escapeHTML(value);
816     this.renderLabel(working, value);
817 
818     // handle right icon
819     if (this.getDelegateProperty('hasContentRightIcon', del)) {
820       key = this.getDelegateProperty('contentRightIconKey', del);
821       value = (key && content) ? (content.get ? content.get(key) : content[key]) : null;
822 
823       if (value) {
824         this.renderRightIcon(working, value);
825         classArray.push('has-right-icon');
826       }
827     } else if (this.get('rightIcon')) {
828       value = this.get('rightIcon');
829       this.renderRightIcon(working, value);
830       classArray.push('has-right-icon');
831     }
832 
833     // handle unread count
834     key = this.getDelegateProperty('contentUnreadCountKey', del);
835     value = (key && content) ? (content.get ? content.get(key) : content[key]) : null;
836     if (!SC.none(value) && (value !== 0)) {
837       this.renderCount(working, value);
838       var digits = ['zero', 'one', 'two', 'three', 'four', 'five'];
839       var valueLength = value.toString().length;
840       var digitsLength = digits.length;
841       var digit = (valueLength < digitsLength) ? digits[valueLength] : digits[digitsLength - 1];
842       classArray.push('has-count', digit + '-digit');
843     } else {
844       // If previously rendered a count, clean up.
845       context.removeClass('has-count');
846     }
847 
848     // handle action
849     key = this.getDelegateProperty('listItemActionProperty', del);
850     value = (key && content) ? (content.get ? content.get(key) : content[key]) : null;
851     if (value) {
852       this.renderAction(working, value);
853       classArray.push('has-action');
854     }
855 
856     // handle branch
857     if (this.getDelegateProperty('hasContentBranch', del)) {
858       key = this.getDelegateProperty('contentIsBranchKey', del);
859       value = (key && content) ? (content.get ? content.get(key) : content[key]) : NO;
860       this.renderBranch(working, value);
861       classArray.push('has-branch');
862     }
863     context.addClass(classArray);
864     context = working.end();
865   },
866 
867   /** @private
868     Adds a disclosure triangle with the appropriate display to the content.
869     This method will only be called if the disclosure state of the view is
870     something other than SC.LEAF_NODE.
871 
872     @param {SC.RenderContext} context the render context
873     @param {Boolean} state YES, NO or SC.MIXED_STATE
874     @returns {void}
875   */
876   renderDisclosure: function (context, state) {
877     var renderer = this.get('theme').disclosureRenderDelegate;
878 
879     context = context.begin('div')
880       .addClass('sc-disclosure-view')
881       .addClass('sc-regular-size')
882       .addClass(this.get('theme').classNames)
883       .addClass(renderer.get('className'));
884 
885     var source = this._disclosureRenderSource;
886     if (!source) {
887       this._disclosureRenderSource = source =
888       SC.Object.create({ renderState: {}, theme: this.get('theme') });
889     }
890 
891     source
892       .set('isSelected', state === SC.BRANCH_OPEN)
893       .set('isEnabled', this.get('isEnabled'))
894       .set('title', '');
895 
896     renderer.render(source, context);
897 
898     context = context.end();
899     this._disclosureRenderDelegate = renderer;
900   },
901 
902   /** @private
903     Adds a checkbox with the appropriate state to the content.  This method
904     will only be called if the list item view is supposed to have a
905     checkbox.
906 
907     @param {SC.RenderContext} context the render context
908     @param {Boolean} state YES, NO or SC.MIXED_STATE
909     @returns {void}
910   */
911   renderCheckbox: function (context, state) {
912     var renderer = this.get('theme').checkboxRenderDelegate;
913 
914     // note: checkbox-view is really not the best thing to do here; we should do
915     // sc-list-item-checkbox; however, themes expect something different, unfortunately.
916     context = context.begin('div')
917       .addClass('sc-checkbox-view')
918       .addClass('sc-regular-size')
919       .addClass(this.get('theme').classNames)
920       .addClass(renderer.get('className'));
921 
922     var source = this._checkboxRenderSource;
923     if (!source) {
924       source = this._checkboxRenderSource =
925       SC.Object.create({ renderState: {}, theme: this.get('theme') });
926     }
927 
928     source
929       .set('isSelected', state && (state !== SC.MIXED_STATE))
930       .set('isEnabled', this.get('isEnabled') && this.get('contentIsEditable'))
931       .set('isActive', this._checkboxIsActive)
932       .set('title', '');
933 
934     renderer.render(source, context);
935     context = context.end();
936 
937     this._checkboxRenderDelegate = renderer;
938   },
939 
940   /** @private
941     Generates an icon for the label based on the content.  This method will
942     only be called if the list item view has icons enabled.  You can override
943     this method to display your own type of icon if desired.
944 
945     @param {SC.RenderContext} context the render context
946     @param {String} icon a URL or class name.
947     @returns {void}
948   */
949   renderIcon: function (context, icon) {
950     // get a class name and url to include if relevant
951     var url = null, className = null, classArray = [];
952     if (icon && SC.ImageView.valueIsUrl(icon)) {
953       url = icon;
954       className = '';
955     } else {
956       className = icon;
957       url = SC.BLANK_IMAGE_URL;
958     }
959 
960     // generate the img element...
961     classArray.push(className, 'icon');
962     context.begin('img')
963             .addClass(classArray)
964             .setAttr('src', url)
965             .end();
966   },
967 
968   /** @private
969    Generates a label based on the content.  You can override this method to
970    display your own type of icon if desired.
971 
972    @param {SC.RenderContext} context the render context
973    @param {String} label the label to display, already HTML escaped.
974    @returns {void}
975   */
976   renderLabel: function (context, label) {
977     context.push('<label>', label || '', '</label>');
978   },
979 
980   /** @private
981     Generates a right icon for the label based on the content.  This method will
982     only be called if the list item view has icons enabled.  You can override
983     this method to display your own type of icon if desired.
984 
985     @param {SC.RenderContext} context the render context
986     @param {String} icon a URL or class name.
987     @returns {void}
988   */
989   renderRightIcon: function (context, icon) {
990     // get a class name and url to include if relevant
991     var url = null,
992       className = null,
993       classArray = [];
994     if (icon && SC.ImageView.valueIsUrl(icon)) {
995       url = icon;
996       className = '';
997     } else {
998       className = icon;
999       url = SC.BLANK_IMAGE_URL;
1000     }
1001 
1002     // generate the img element...
1003     classArray.push('right-icon', className);
1004     context.begin('img')
1005       .addClass(classArray)
1006       .setAttr('src', url)
1007     .end();
1008   },
1009 
1010   /** @private
1011    Generates an unread or other count for the list item.  This method will
1012    only be called if the list item view has counts enabled.  You can
1013    override this method to display your own type of counts if desired.
1014 
1015    @param {SC.RenderContext} context the render context
1016    @param {Number} count the count
1017    @returns {void}
1018   */
1019   renderCount: function (context, count) {
1020     context.push('<span class="count"><span class="inner">',
1021                   count.toString(), '</span></span>');
1022   },
1023 
1024   /** @private
1025     Generates the html string used to represent the action item for your
1026     list item.  override this to return your own custom HTML
1027 
1028     @param {SC.RenderContext} context the render context
1029     @param {String} actionClassName the name of the action item
1030     @returns {void}
1031   */
1032   renderAction: function (context, actionClassName) {
1033     context.push('<img src="', SC.BLANK_IMAGE_URL, '" class="action" />');
1034   },
1035 
1036   /** @private
1037    Generates the string used to represent the branch arrow. override this to
1038    return your own custom HTML
1039 
1040    @param {SC.RenderContext} context the render context
1041    @param {Boolean} hasBranch YES if the item has a branch
1042    @returns {void}
1043   */
1044   renderBranch: function (context, hasBranch) {
1045     var classArray = [];
1046     classArray.push('branch', hasBranch ? 'branch-visible' : 'branch-hidden');
1047     context.begin('span')
1048           .addClass(classArray)
1049           .push(' ')
1050           .end();
1051   }
1052 
1053 });
1054 
1055 SC.ListItemView._deprecatedRenderWarningHasBeenIssued = false;
1056