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 /** @class 10 11 A RadioView is used to create a group of radio buttons. The user can use 12 these buttons to pick from a choice of options. 13 14 This view renders simulated radio buttons that can display a mixed state and 15 has other features not found in platform-native controls. 16 17 The radio buttons themselves are designed to be styled using CSS classes with 18 the following structure: 19 20 <label class="sc-radio-button"> 21 <img class="button" src="some_image.gif"/> 22 <input type="radio" name="<sc-guid>" value=""/> 23 <span class="sc-button-label">Label for button1</span> 24 </label> 25 26 Setting up a RadioView accepts a number of properties, for example: 27 28 radio: SC.RadioView.design({ 29 items: [ 30 { 31 title: "Red", 32 value: "red", 33 enabled: YES, 34 icon: "button_red" 35 },{ 36 title: "Green", 37 value: "green", 38 enabled: YES, 39 icon: 'button_green' 40 } 41 ], 42 value: 'red', 43 itemTitleKey: 'title', 44 itemValueKey: 'value', 45 itemIconKey: 'icon', 46 itemIsEnabledKey: 'enabled', 47 isEnabled: YES, 48 layoutDirection: SC.LAYOUT_HORIZONTAL 49 }) 50 51 The items array can contain either strings, or as in the example above a 52 hash. When using a hash, make sure to also specify the itemTitleKey 53 and itemValueKey you are using. Similarly, you will have to provide 54 itemIconKey if you are using icons radio buttons. The individual items 55 enabled property is YES by default, and the icon is optional. 56 57 @extends SC.View 58 @extends SC.Control 59 @since SproutCore 1.0 60 */ 61 SC.RadioView = SC.View.extend(SC.Control, 62 /** @scope SC.RadioView.prototype */{ 63 64 /** 65 @field 66 @type Boolean 67 @default YES 68 @observes isEnabled 69 */ 70 acceptsFirstResponder: function() { 71 if (SC.FOCUS_ALL_CONTROLS) { return this.get('isEnabledInPane'); } 72 return NO; 73 }.property('isEnabledInPane'), 74 75 /** 76 @type Array 77 @default ['sc-radio-view'] 78 @see SC.View#classNames 79 */ 80 classNames: ['sc-radio-view'], 81 82 /** 83 The WAI-ARIA role for a group of radio buttons. 84 85 @type String 86 @default 'radiogroup' 87 @readOnly 88 */ 89 ariaRole: 'radiogroup', 90 91 /** 92 @type Array 93 @default ['displayItems', 'layoutDirection'] 94 @see SC.View#displayProperties 95 */ 96 displayProperties: ['displayItems', 'layoutDirection'], 97 98 /** 99 @type String 100 @default 'radioGroupRenderDelegate' 101 */ 102 renderDelegateName: 'radioGroupRenderDelegate', 103 104 // .......................................................... 105 // Properties 106 // 107 108 /** 109 If items property is a hash, specify which property will function as 110 the ariaLabeledBy with this itemAriaLabeledByKey property.ariaLabeledBy is used 111 as the WAI-ARIA attribute for the radio view. This property is assigned to 112 'aria-labelledby' attribute, which defines a string value that labels the 113 element. Used to support voiceover. It should be assigned a non-empty string, 114 if the 'aria-labelledby' attribute has to be set for the element. 115 116 @type String 117 @default null 118 */ 119 itemAriaLabeledByKey: null, 120 121 /** 122 If items property is a hash, specify which property will function as 123 the ariaLabeled with this itemAriaLabelKey property.ariaLabel is used 124 as the WAI-ARIA attribute for the radio view. This property is assigned to 125 'aria-label' attribute, which defines a string value that labels the 126 element. Used to support voiceover. It should be assigned a non-empty string, 127 if the 'aria-label' attribute has to be set for the element. 128 129 @type String 130 @default null 131 */ 132 itemAriaLabelKey: null, 133 134 /** 135 The value of the currently selected item, and which will be checked in the 136 UI. This can be either a string or an array with strings for checking 137 multiple values. 138 139 @type Object|String 140 @default null 141 */ 142 value: null, 143 144 /** 145 This property indicates how the radio buttons are arranged. Possible values: 146 147 - SC.LAYOUT_VERTICAL 148 - SC.LAYOUT_HORIZONTAL 149 150 @type String 151 @default SC.LAYOUT_VERTICAL 152 */ 153 layoutDirection: SC.LAYOUT_VERTICAL, 154 155 /** 156 @type Boolean 157 @default YES 158 */ 159 escapeHTML: YES, 160 161 /** 162 The items property can be either an array with strings, or a 163 hash. When using a hash, make sure to also specify the appropriate 164 itemTitleKey, itemValueKey, itemIsEnabledKey and itemIconKey. 165 166 @type Array 167 @default [] 168 */ 169 items: [], 170 171 /** 172 If items property is a hash, specify which property will function as 173 the title with this itemTitleKey property. 174 175 @type String 176 @default null 177 */ 178 itemTitleKey: null, 179 180 /** 181 If items property is a hash, specify which property will function as 182 the item width with this itemWidthKey property. This is only used when 183 layoutDirection is set to SC.LAYOUT_HORIZONTAL and can be used to override 184 the default value provided by the framework or theme CSS. 185 186 @type String 187 @default null 188 */ 189 itemWidthKey: null, 190 191 /** 192 If items property is a hash, specify which property will function as 193 the value with this itemValueKey property. 194 195 @type String 196 @default null 197 */ 198 itemValueKey: null, 199 200 /** 201 If items property is a hash, specify which property will function as 202 the value with this itemIsEnabledKey property. 203 204 @type String 205 @default null 206 */ 207 itemIsEnabledKey: null, 208 209 /** 210 If items property is a hash, specify which property will function as 211 the value with this itemIconKey property. 212 213 @type String 214 @default null 215 */ 216 itemIconKey: null, 217 218 /** @private 219 If the items array itself changes, add/remove observer on item... 220 */ 221 itemsDidChange: function() { 222 if (this._items) { 223 this._items.removeObserver('[]', this, this.itemContentDidChange); 224 } 225 this._items = this.get('items'); 226 if (this._items) { 227 this._items.addObserver('[]', this, this.itemContentDidChange); 228 } 229 this.itemContentDidChange(); 230 }.observes('items'), 231 232 /** @private 233 Invoked whenever the item array or an item in the array is changed. 234 This method will regenerate the list of items. 235 */ 236 itemContentDidChange: function() { 237 // Force regeneration of buttons 238 this._renderAsFirstTime = YES; 239 240 this.notifyPropertyChange('displayItems'); 241 }, 242 243 // .......................................................... 244 // PRIVATE SUPPORT 245 // 246 247 /** @private 248 Data Sources for radioRenderDelegates, as required by radioGroupRenderDelegate. 249 */ 250 displayItems: function() { 251 var items = this.get('items'), 252 viewValue = this.get('value'), 253 isArray = SC.isArray(viewValue), 254 loc = this.get('localize'), 255 titleKey = this.get('itemTitleKey'), 256 valueKey = this.get('itemValueKey'), 257 widthKey = this.get('itemWidthKey'), 258 isHorizontal = this.get('layoutDirection') === SC.LAYOUT_HORIZONTAL, 259 isEnabledKey = this.get('itemIsEnabledKey'), 260 iconKey = this.get('itemIconKey'), 261 ariaLabeledByKey = this.get('itemAriaLabeledByKey'), 262 ariaLabelKey = this.get('itemAriaLabelKey'), 263 ret = this._displayItems || [], max = (items)? items.get('length') : 0, 264 item, title, width, value, idx, isEnabled, icon, sel, active, 265 ariaLabeledBy, ariaLabel; 266 267 for(idx=0;idx<max;idx++) { 268 item = items.objectAt(idx); 269 270 // if item is an array, just use the items... 271 if (SC.typeOf(item) === SC.T_ARRAY) { 272 title = item[0]; 273 value = item[1]; 274 275 // otherwise, possibly use titleKey,etc. 276 } else if (item) { 277 // get title. either use titleKey or try to convert the value to a 278 // string. 279 if (titleKey) { 280 title = item.get ? item.get(titleKey) : item[titleKey]; 281 } else title = (item.toString) ? item.toString() : null; 282 283 if (widthKey && isHorizontal) { 284 width = item.get ? item.get(widthKey) : item[widthKey]; 285 } 286 287 if (valueKey) { 288 value = item.get ? item.get(valueKey) : item[valueKey]; 289 } else value = item; 290 291 if (isEnabledKey) { 292 isEnabled = item.get ? item.get(isEnabledKey) : item[isEnabledKey]; 293 } else isEnabled = YES; 294 295 if (iconKey) { 296 icon = item.get ? item.get(iconKey) : item[iconKey]; 297 } else icon = null; 298 299 if (ariaLabeledByKey) { 300 ariaLabeledBy = item.get ? item.get(ariaLabeledByKey) : item[ariaLabeledByKey]; 301 } else ariaLabeledBy = null; 302 303 if (ariaLabelKey) { 304 ariaLabel = item.get ? item.get(ariaLabelKey) : item[ariaLabelKey]; 305 } else ariaLabel = null; 306 307 // if item is nil, use some defaults... 308 } else { 309 title = value = icon = null; 310 isEnabled = NO; 311 } 312 313 // it can only be enabled if the radio view itself is enabled 314 isEnabled = isEnabled && this.get('isEnabled'); 315 316 if (item) { 317 sel = (isArray) ? (viewValue.indexOf(value) >= 0) : (viewValue === value); 318 } else { 319 sel = NO; 320 } 321 322 // localize title if needed 323 if (loc) title = SC.String.loc(title); 324 ret.push(SC.Object.create({ 325 title: title, 326 icon: icon, 327 width: width, 328 value: value, 329 330 isEnabled: isEnabled, 331 isSelected: (isArray && viewValue.indexOf(value) >= 0 && viewValue.length === 1) || (viewValue === value), 332 isMixed: (isArray && viewValue.indexOf(value) >= 0), 333 isActive: this._activeRadioButton === idx, 334 theme: this.get('theme'), 335 renderState: {} 336 })); 337 } 338 339 return ret; // done! 340 }.property('isEnabled', 'value', 'items', 'itemTitleKey', 'itemWidthKey', 'itemValueKey', 'itemIsEnabledKey', 'localize', 'itemIconKey','itemAriaLabeledByKey', 'itemAriaLabelKey').cacheable(), 341 342 /** @private 343 If the user clicks on of the items mark it as active on mouseDown unless 344 is disabled. 345 346 Save the element that was clicked on so we can remove the active state on 347 mouseUp. 348 */ 349 mouseDown: function(evt) { 350 // Fast path, reject secondary clicks. 351 if (evt.which && evt.which !== 1) return false; 352 353 if (!this.get('isEnabledInPane')) return YES; 354 355 var delegate = this.get('renderDelegate'), proxy = this.get('renderDelegateProxy'), 356 elem = this.$(), 357 index = delegate.indexForEvent(proxy, elem, evt); 358 359 this._activeRadioButton = index; 360 361 if (index !== undefined) { 362 var item = this.get('displayItems')[index]; 363 if (item.get('isEnabled')) { 364 item.set('isActive', YES); 365 delegate.updateRadioAtIndex(proxy, elem, index); 366 } 367 } 368 369 // even if radiobuttons are not set to get firstResponder, allow default 370 // action, that way textfields loose focus as expected. 371 evt.allowDefault(); 372 return YES; 373 }, 374 375 /** @private 376 If we have a radio element that was clicked on previously, make sure we 377 remove the active state. Then update the value if the item clicked is 378 enabled. 379 */ 380 mouseUp: function(evt) { 381 if (!this.get('isEnabledInPane')) return YES; 382 383 var delegate = this.get('renderDelegate'), proxy = this.get('renderDelegateProxy'), 384 elem = this.$(), 385 displayItems = this.get('displayItems'), 386 index = delegate.indexForEvent(proxy, elem, evt); 387 388 if (this._activeRadioButton !== undefined && index !== this._activeRadioButton) { 389 displayItems[this._activeRadioButton].set('isActive', NO); 390 delegate.updateRadioAtIndex(proxy, elem, this._activeRadioButton); 391 } 392 393 this._activeRadioButton = undefined; 394 395 if (index !== undefined) { 396 var item = this.get('displayItems')[index]; 397 if (item.get('isEnabled')) { 398 item.set('isActive', NO); 399 delegate.updateRadioAtIndex(proxy, elem, index); 400 this.set('value', item.value); 401 } 402 } 403 404 evt.allowDefault(); 405 return YES; 406 }, 407 408 keyDown: function(evt) { 409 if(!this.get('isEnabledInPane')) return YES; 410 // handle tab key 411 if (evt.which === 9 || evt.keyCode === 9) { 412 var view = evt.shiftKey ? this.get('previousValidKeyView') : this.get('nextValidKeyView'); 413 if(view) view.becomeFirstResponder(); 414 else evt.allowDefault(); 415 return YES ; // handled 416 } 417 if (evt.which >= 37 && evt.which <= 40){ 418 419 var delegate = this.get('renderDelegate'), proxy = this.get('renderDelegateProxy'), 420 elem = this.$(), 421 displayItems = this.get('displayItems'), 422 val = this.get('value'); 423 for(var i= 0, iLen = displayItems.length; i<iLen; i++){ 424 if(val === displayItems[i].value) break; 425 } 426 427 428 if (evt.which === 37 || evt.which === 38 ){ 429 if(i<=0) i = displayItems.length-1; 430 else i--; 431 } 432 if (evt.which === 39 || evt.which === 40 ){ 433 if(i>=displayItems.length-1) i = 0; 434 else i++; 435 } 436 delegate.updateRadioAtIndex(proxy, elem, i); 437 this.set('value', displayItems[i].value); 438 } 439 evt.allowDefault(); 440 441 return NO; 442 }, 443 444 445 /** @private */ 446 touchStart: function(evt) { 447 return this.mouseDown(evt); 448 }, 449 450 /** @private */ 451 touchEnd: function(evt) { 452 return this.mouseUp(evt); 453 } 454 455 }); 456