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