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   @static
 10   @constant
 11   @type String
 12 */
 13 SC.TOGGLE_BEHAVIOR = 'toggle';
 14 
 15 /**
 16   @static
 17   @constant
 18   @type String
 19 */
 20 SC.PUSH_BEHAVIOR = 'push';
 21 
 22 /**
 23   @static
 24   @constant
 25   @type String
 26 */
 27 SC.TOGGLE_ON_BEHAVIOR = 'on';
 28 
 29 /**
 30   @static
 31   @constant
 32   @type String
 33 */
 34 SC.TOGGLE_OFF_BEHAVIOR = 'off';
 35 
 36 /**
 37   @static
 38   @constant
 39   @type String
 40 */
 41 SC.HOLD_BEHAVIOR = 'hold';
 42 
 43 /** @class
 44 
 45   Implements a push-button-style button.  This class is used to implement
 46   both standard push buttons and tab-style controls.  See also SC.CheckboxView
 47   and SC.RadioView which are implemented as field views, but can also be
 48   treated as buttons.
 49 
 50   By default, a button uses the SC.Control mixin which will apply CSS
 51   classnames when the state of the button changes:
 52 
 53    - `active` -- when button is active
 54    - `sel` -- when button is toggled to a selected state
 55 
 56   @extends SC.View
 57   @extends SC.Control
 58   @since SproutCore 1.0
 59 */
 60 SC.ButtonView = SC.View.extend(SC.ActionSupport, SC.Control,
 61 /** @scope SC.ButtonView.prototype */ {
 62 
 63   /**
 64     Tied to the isEnabledInPane state
 65 
 66     @type Boolean
 67     @default YES
 68   */
 69   acceptsFirstResponder: function() {
 70     if (SC.FOCUS_ALL_CONTROLS) { return this.get('isEnabledInPane'); }
 71     return NO;
 72   }.property('isEnabledInPane'),
 73 
 74   /**
 75     The name of the method to call when the button is pressed.
 76 
 77     This property is used in conjunction with the `target` property to execute a method when a
 78     regular button is pressed. If you do not set a target, then pressing the button will cause a
 79     search of the responder chain for a view that implements the action named. If you do set a
 80     target, then the button will only try to call the method on that target.
 81 
 82     The action method of the target should implement the following signature:
 83 
 84         action: function (sender) {
 85           // Return value is ignored by SC.ButtonView.
 86         }
 87 
 88     Therefore, if a target needs to know which button called its action, it should look to the
 89     `sender` argument.
 90 
 91     *NOTE:* This property is not relevant when the button is used in toggle mode. Toggle mode only
 92     modifies the `value` of the button without triggering actions.
 93 
 94     @type String
 95     @default null
 96     @see SC.ActionSupport
 97   */
 98   action: null,
 99 
100   /**
101     @type Array
102     @default ['sc-button-view']
103     @see SC.View#classNames
104   */
105   classNames: ['sc-button-view'],
106 
107   /**
108     Whether the title and toolTip will be escaped to avoid HTML injection attacks
109     or not.
110 
111     You should only disable this option if you are sure you are displaying
112     non-user generated text.
113 
114     Note: this is not an observed display property.  If you change it after
115     rendering, you should call `displayDidChange` on the view to update the layer.
116 
117     @type Boolean
118     @default true
119    */
120   escapeHTML: true,
121 
122   /**
123     The target to invoke the action on when the button is pressed.
124 
125     If you set this target, the action will be called on the target object directly when the button
126     is clicked.  If you leave this property set to `null`, then the responder chain will be
127     searched for a view that implements the action when the button is pressed.
128 
129     The action method of the target should implement the following signature:
130 
131         action: function (sender) {
132           // Return value is ignored by SC.ButtonView.
133         }
134 
135     Therefore, if a target needs to know which button called its action, it should look to the
136     `sender` argument.
137 
138     *NOTE:* This property is not relevant when the button is used in toggle mode. Toggle mode only
139     modifies the `value` of the button without triggering actions.
140 
141     @type Object
142     @default null
143     @see SC.ActionSupport
144   */
145   target: null,
146 
147   /**
148     The theme to apply to the button. By default, a subtheme with the name of
149     'square' is created for backwards-compatibility.
150 
151     @type String
152     @default 'square'
153   */
154   themeName: 'square',
155 
156 
157   // ..........................................................
158   // Value Handling
159   //
160 
161   /**
162     Used to automatically update the state of the button view for toggle style
163     buttons.
164 
165     For toggle style buttons, you can set the value and it will be used to
166     update the isSelected state of the button view.  The value will also
167     change as the user selects or deselects.  You can control which values
168     the button will treat as `isSelected` by setting the `toggleOnValue` and
169     `toggleOffValue`.  Alternatively, if you leave these properties set to
170     `YES` or `NO`, the button will do its best to convert a value to an
171     appropriate state:
172 
173      - `null`, `false`, `0` -- `isSelected = false`
174      - any other single value -- `isSelected = true`
175      - array -- if all values are the same state, that state; otherwise `MIXED`.
176 
177     @type Object
178     @default null
179   */
180   value: null,
181 
182   /**
183     Value of a selected toggle button.
184 
185     For a toggle button, set this to any object value you want. The button
186     will be selected if the value property equals the targetValue. If the
187     value is an array of multiple items that contains the targetValue, then
188     the button will be set to a mixed state.
189 
190     default is YES
191 
192     @type Boolean|Object
193     @default YES
194   */
195   toggleOnValue: YES,
196 
197   /**
198     Value of an unselected toggle button.
199 
200     For a toggle button, set this to any object value you want.  When the
201     user toggle's the button off, the value of the button will be set to this
202     value.
203 
204     @type Boolean|Object
205     @default NO
206   */
207   toggleOffValue: NO,
208 
209 
210   // ..........................................................
211   // Title Handling
212   //
213 
214   /**
215     If YES, then the title will be localized.
216 
217     @type Boolean
218     @default NO
219   */
220   localize: NO,
221 
222   /** @private */
223   localizeBindingDefault: SC.Binding.bool(),
224 
225   /**
226     The button title.  If localize is `YES`, then this should be the
227     localization key to display.  Otherwise, this will be the actual string
228     displayed in the title.  This property is observable and bindable.
229 
230     @type String
231     @default ""
232   */
233   title: "",
234 
235   /**
236     If set, the title property will be updated automatically
237     from the content using the key you specify.
238 
239     @type String
240     @default null
241   */
242   contentTitleKey: null,
243 
244   /**
245     The button icon. Set this to either a URL or a CSS class name (for
246     spriting). Note that if you pass a URL, it must contain at
247     least one slash to be detected as such.
248 
249     @type String
250     @default null
251   */
252   icon: null,
253 
254   /**
255     If you set this property, the icon will be updated automatically from the
256     content using the key you specify.
257 
258     @type String
259     @default null
260   */
261   contentIconKey: null,
262 
263   /**
264     If YES, button will attempt to display an ellipsis if the title cannot
265     fit inside of the visible area. This feature is not available on all
266     browsers.
267 
268     Note: this is not an observed display property.  If you change it after
269     rendering, you should call `displayDidChange` on the view to update the layer.
270 
271     @type Boolean
272     @default YES
273   */
274   needsEllipsis: YES,
275 
276   /**
277     This is generated by localizing the title property if necessary.
278 
279     @type String
280     @observes 'title'
281     @observes 'localize'
282   */
283   displayTitle: function() {
284     var ret = this.get('title');
285     return (ret && this.get('localize')) ? SC.String.loc(ret) : (ret || '');
286   }.property('title','localize').cacheable(),
287 
288   /**
289     The key equivalent that should trigger this button on the page.
290 
291     @type String
292     @default null
293   */
294   keyEquivalent: null,
295 
296 
297   // ..........................................................
298   // BEHAVIOR
299   //
300 
301   /**
302     The behavioral mode of this button.
303 
304     Possible values are:
305 
306      - `SC.PUSH_BEHAVIOR` -- Pressing the button will trigger an action tied to the
307        button. Does not change the value of the button.
308      - `SC.TOGGLE_BEHAVIOR` -- Pressing the button will invert the current value of
309        the button. If the button has a mixed value, it will be set to true.
310      - `SC.TOGGLE_ON_BEHAVIOR` -- Pressing the button will set the current state to
311        true no matter the previous value.
312      - `SC.TOGGLE_OFF_BEHAVIOR` -- Pressing the button will set the current state to
313        false no matter the previous value.
314      - `SC.HOLD_BEHAVIOR` -- Pressing the button will cause the action to repeat at a
315        regular interval specified by 'holdInterval'
316 
317     @type String
318     @default SC.PUSH_BEHAVIOR
319   */
320   buttonBehavior: SC.PUSH_BEHAVIOR,
321 
322   /*
323     If buttonBehavior is `SC.HOLD_BEHAVIOR`, this specifies, in milliseconds,
324     how often to trigger the action. Ignored for other behaviors.
325 
326     @type Number
327     @default 100
328   */
329   holdInterval: 100,
330 
331   /**
332     If YES, then this button will be triggered when you hit return.
333 
334     This is the same as setting the `keyEquivalent` to 'return'.  This will also
335     apply the "def" classname to the button.
336 
337     @type Boolean
338     @default NO
339   */
340   isDefault: NO,
341   isDefaultBindingDefault: SC.Binding.oneWay().bool(),
342 
343   /**
344     If YES, then this button will be triggered when you hit escape.
345     This is the same as setting the keyEquivalent to 'escape'.
346 
347     @type Boolean
348     @default NO
349   */
350   isCancel: NO,
351   isCancelBindingDefault: SC.Binding.oneWay().bool(),
352 
353   /*
354     TODO When is this property ever changed? Is this redundant with
355     render delegates since it can now be turned on on a theme-by-theme
356     basis? --TD
357   */
358   /**
359     If YES, use a focus ring.
360 
361     @type Boolean
362     @default NO
363   */
364   supportFocusRing: NO,
365 
366   // ..........................................................
367   // Auto Resize Support
368   //
369   //
370   // These properties are provided so that SC.AutoResize can be mixed in
371   // to enable automatic resizing of the button.
372   //
373 
374   /** @private */
375   supportsAutoResize: YES,
376 
377   /*
378     TODO get this from the render delegate so other elements may be used.
379   */
380   /** @private */
381   autoResizeLayer: function() {
382     var ret = this.invokeRenderDelegateMethod('getRenderedAutoResizeLayer', this.$());
383     return ret || this.get('layer');
384   }.property('layer').cacheable(),
385 
386   /** @private */
387   autoResizeText: function() {
388     return this.get('displayTitle');
389   }.property('displayTitle').cacheable(),
390 
391   /**
392     The padding to add to the measured size of the text to arrive at the measured
393     size for the view.
394 
395     `SC.ButtonView` gets this from its render delegate, but if not supplied, defaults
396     to 10.
397 
398     @default 10
399     @type Number
400   */
401   autoResizePadding: SC.propertyFromRenderDelegate('autoResizePadding', 10),
402 
403 
404   // TODO: What the hell is this? --TD
405   _labelMinWidthIE7: 0,
406 
407   /**
408     Called when the user presses a shortcut key, such as return or cancel,
409     associated with this button.
410 
411     Highlights the button to show that it is being triggered, then, after a
412     delay, performs the button's action.
413 
414     Does nothing if the button is disabled.
415 
416     @param {Event} evt
417     @returns {Boolean} YES if successful, NO otherwise
418   */
419   triggerActionAfterDelay: function(evt) {
420     // If this button is disabled, we have nothing to do
421     if (!this.get('isEnabledInPane')) return NO;
422 
423     // Set active state of the button so it appears highlighted
424     this.set('isActive', YES);
425 
426     // Invoke the actual action method after a small delay to give the user a
427     // chance to see the highlight. This is especially important if the button
428     // closes a pane, for example.
429     this.invokeLater('triggerAction', SC.ButtonView.TRIGGER_DELAY, evt);
430     return YES;
431   },
432 
433   /** @private
434     Called by triggerActionAfterDelay; this method actually
435     performs the action and restores the button's state.
436 
437     @param {Event} evt
438   */
439   triggerAction: function(evt) {
440     this._action(evt, YES);
441     this.didTriggerAction();
442     this.set('isActive', NO);
443   },
444 
445   /**
446     Callback called anytime the button's action is triggered.  You can
447     implement this method in your own subclass to perform any cleanup needed
448     after an action is performed.
449   */
450   didTriggerAction: function() {},
451 
452 
453   // ................................................................
454   // INTERNAL SUPPORT
455   //
456 
457   /** @private - save keyEquivalent for later use */
458   init: function() {
459     sc_super();
460 
461     var keyEquivalent = this.get('keyEquivalent');
462     // Cache the key equivalent. The key equivalent is saved so that if,
463     // for example, isDefault is changed from YES to NO, the old key
464     // equivalent can be restored.
465     if (keyEquivalent) {
466       this._defaultKeyEquivalent = keyEquivalent;
467     }
468 
469     // if value is not null, update isSelected to match value.  If value is
470     // null, we assume you may be using isSelected only.
471     if (!SC.none(this.get('value'))) this._button_valueDidChange();
472   },
473 
474   /**
475     The WAI-ARIA role of the button.
476 
477     @type String
478     @default 'button'
479     @readOnly
480   */
481   ariaRole: 'button',
482 
483   /**
484     The following properties affect how `SC.ButtonView` is rendered, and will
485     cause the view to be rerendered if they change.
486 
487     Note: 'value', 'isDefault', 'isCancel' are also display properties, but are
488     observed separately.
489 
490     @type Array
491     @default ['icon', 'displayTitle', 'displayToolTip', 'supportFocusRing', 'buttonBehavior']
492   */
493   displayProperties: ['icon', 'displayTitle', 'displayToolTip', 'supportFocusRing', 'buttonBehavior'],
494 
495   /**
496     The name of the render delegate in the theme that should be used to
497     render the button.
498 
499     In this case, the 'button' property will be retrieved from the theme and
500     set to the render delegate of this view.
501 
502     @type String
503     @default 'buttonRenderDelegate'
504   */
505   renderDelegateName: 'buttonRenderDelegate',
506 
507   contentKeys: {
508     'contentValueKey': 'value',
509     'contentTitleKey': 'title',
510     'contentIconKey': 'icon'
511   },
512 
513   /**
514     Handle a key equivalent if set.  Trigger the default action for the
515     button.  Depending on the implementation this may vary.
516 
517     @param {String} keystring
518     @param {SC.Event} evt
519     @returns {Boolean}  YES if handled, NO otherwise
520   */
521   performKeyEquivalent: function(keystring, evt) {
522     //If this is not visible
523     if (!this.get('isVisibleInWindow')) return NO;
524 
525     if (!this.get('isEnabledInPane')) return NO;
526     var equiv = this.get('keyEquivalent');
527 
528     // button has defined a keyEquivalent and it matches!
529     // if triggering succeeded, true will be returned and the operation will
530     // be handled (i.e performKeyEquivalent will cease crawling the view
531     // tree)
532     if (equiv) {
533       if (equiv === keystring) return this.triggerAction(evt);
534 
535     // should fire if isDefault OR isCancel.  This way if isDefault AND
536     // isCancel, responds to both return and escape
537     } else if ((this.get('isDefault') && (keystring === 'return')) ||
538         (this.get('isCancel') && (keystring === 'escape'))) {
539           return this.triggerAction(evt);
540     }
541 
542     return NO; // did not handle it; keep searching
543   },
544 
545   // ..........................................................
546   // VALUE <-> isSelected STATE MANAGEMENT
547   //
548 
549   /**
550     This is the standard logic to compute a proposed isSelected state for a
551     new value.  This takes into account the `toggleOnValue`/`toggleOffValue`
552     properties, among other things.  It may return `YES`, `NO`, or
553     `SC.MIXED_STATE`.
554 
555     @param {Object} value
556     @returns {Boolean} return state
557   */
558   computeIsSelectedForValue: function(value) {
559     var targetValue = this.get('toggleOnValue'), state, next ;
560 
561     if (SC.typeOf(value) === SC.T_ARRAY) {
562 
563       // treat a single item array like a single value
564       if (value.length === 1) {
565         state = (value[0] == targetValue) ;
566 
567       // for a multiple item array, check the states of all items.
568       } else {
569         state = null;
570         value.find(function(x) {
571           next = (x == targetValue) ;
572           if (state === null) {
573             state = next ;
574           } else if (next !== state) state = SC.MIXED_STATE ;
575           return state === SC.MIXED_STATE ; // stop when we hit a mixed state.
576         });
577       }
578 
579     // for single values, just compare to the toggleOnValue...use truthiness
580     } else {
581       if(value === SC.MIXED_STATE) state = SC.MIXED_STATE;
582       else state = (value === targetValue) ;
583     }
584     return state ;
585   },
586 
587   /** @private
588     Whenever the button value changes, update the selected state to match.
589   */
590   _button_valueDidChange: function() {
591     var value = this.get('value'),
592         state = this.computeIsSelectedForValue(value);
593     this.set('isSelected', state) ; // set new state...
594 
595     // value acts as a display property
596     this.displayDidChange();
597   }.observes('value'),
598 
599   /** @private
600     Whenever the selected state is changed, make sure the button value is
601     also updated.  Note that this may be called because the value has just
602     changed.  In that case this should do nothing.
603   */
604   _button_isSelectedDidChange: function() {
605     var newState = this.get('isSelected'),
606         curState = this.computeIsSelectedForValue(this.get('value'));
607 
608     // fix up the value, but only if computed state does not match.
609     // never fix up value if isSelected is set to MIXED_STATE since this can
610     // only come from the value.
611     if ((newState !== SC.MIXED_STATE) && (curState !== newState)) {
612       var valueKey = (newState) ? 'toggleOnValue' : 'toggleOffValue' ;
613       this.set('value', this.get(valueKey));
614     }
615   }.observes('isSelected'),
616 
617 
618   /** @private
619     Used to store the keyboard equivalent.
620 
621     Setting the isDefault property to YES, for example, will cause the
622     `keyEquivalent` property to 'return'. This cached value is used to restore
623     the `keyEquivalent` property if isDefault is set back to NO.
624 
625     @type String
626   */
627   _defaultKeyEquivalent: null,
628 
629   /** @private
630 
631     Whenever the isDefault or isCancel property changes, re-render and change
632     the keyEquivalent property so that we respond to the return or escape key.
633   */
634   _isDefaultOrCancelDidChange: function() {
635     var isDefault = !!this.get('isDefault'),
636         isCancel = !isDefault && this.get('isCancel') ;
637 
638     if (isDefault) {
639       this.set('keyEquivalent', 'return'); // change the key equivalent
640     } else if (isCancel) {
641       this.set('keyEquivalent', 'escape') ;
642     } else {
643       // Restore the default key equivalent
644       this.set('keyEquivalent', this._defaultKeyEquivalent);
645     }
646 
647     // isDefault and isCancel act as display properties
648     this.displayDidChange();
649   }.observes('isDefault', 'isCancel'),
650 
651   /** @private
652     On mouse down, set active only if enabled.
653   */
654   mouseDown: function(evt) {
655     // Fast path, reject secondary clicks.
656     if (evt.which !== 1) return false;
657 
658     var buttonBehavior = this.get('buttonBehavior');
659 
660     if (!this.get('isEnabledInPane')) return YES ; // handled event, but do nothing
661     this.set('isActive', YES);
662     this._isMouseDown = YES;
663 
664     if (buttonBehavior === SC.HOLD_BEHAVIOR) {
665       this._action(evt);
666     } else if (!this._isFocused && (buttonBehavior!==SC.PUSH_BEHAVIOR)) {
667       this._isFocused = YES ;
668       this.becomeFirstResponder();
669     }
670 
671     return YES;
672   },
673 
674   /** @private
675     Remove the active class on mouseExited if mouse is down.
676   */
677   mouseExited: function(evt) {
678     if (this._isMouseDown) {
679       this.set('isActive', NO);
680     }
681     return YES;
682   },
683 
684   /** @private
685     If mouse was down and we renter the button area, set the active state again.
686   */
687   mouseEntered: function(evt) {
688     if (this._isMouseDown) {
689       this.set('isActive', YES);
690     }
691     return YES;
692   },
693 
694   /** @private
695     ON mouse up, trigger the action only if we are enabled and the mouse was released inside of the view.
696   */
697   mouseUp: function(evt) {
698     if (this._isMouseDown) this.set('isActive', NO); // track independently in case isEnabledInPane has changed
699     this._isMouseDown = false;
700 
701     if (this.get('buttonBehavior') !== SC.HOLD_BEHAVIOR) {
702       var inside = this.$().within(evt.target);
703       if (inside && this.get('isEnabledInPane')) this._action(evt) ;
704     }
705 
706     return YES ;
707   },
708 
709   /** @private */
710   touchStart: function(touch){
711     var buttonBehavior = this.get('buttonBehavior');
712 
713     if (!this.get('isEnabledInPane')) return YES ; // handled event, but do nothing
714     this.set('isActive', YES);
715 
716     if (buttonBehavior === SC.HOLD_BEHAVIOR) {
717       this._action(touch);
718     } else if (!this._isFocused && (buttonBehavior!==SC.PUSH_BEHAVIOR)) {
719       this._isFocused = YES ;
720       this.becomeFirstResponder();
721     }
722 
723     // don't want to do whatever default is...
724     touch.preventDefault();
725 
726     return YES;
727   },
728 
729   /** @private */
730   touchesDragged: function(evt, touches) {
731     if (!this.touchIsInBoundary(evt)) {
732       if (!this._touch_exited) this.set('isActive', NO);
733       this._touch_exited = YES;
734     } else {
735       if (this._touch_exited) this.set('isActive', YES);
736       this._touch_exited = NO;
737     }
738 
739     evt.preventDefault();
740     return YES;
741   },
742 
743   /** @private */
744   touchEnd: function(touch){
745     this._touch_exited = NO;
746     this.set('isActive', NO); // track independently in case isEnabledInPane has changed
747 
748     if (this.get('buttonBehavior') !== SC.HOLD_BEHAVIOR) {
749       if (this.touchIsInBoundary(touch) && this.get('isEnabledInPane')) {
750         this._action();
751       }
752     }
753 
754     touch.preventDefault();
755     return YES ;
756   },
757 
758   /** @private */
759   keyDown: function(evt) {
760     // handle tab key
761      if(!this.get('isEnabledInPane')) return YES;
762     if (evt.which === 9 || evt.keyCode === 9) {
763       var view = evt.shiftKey ? this.get('previousValidKeyView') : this.get('nextValidKeyView');
764       if(view) view.becomeFirstResponder();
765       else evt.allowDefault();
766       return YES ; // handled
767     }
768     if (evt.which === 13 || evt.which === 32) {
769       this.triggerActionAfterDelay(evt);
770       return YES ; // handled
771     }
772 
773     // let other keys through to browser
774     evt.allowDefault();
775 
776     return NO;
777   },
778 
779   /** @private
780     Perform an action based on the behavior of the button.
781 
782      - toggle behavior: switch to on/off state
783      - on behavior: turn on.
784      - off behavior: turn off.
785      - otherwise: invoke target/action
786   */
787   _action: function(evt, skipHoldRepeat) {
788     switch(this.get('buttonBehavior')) {
789 
790     // When toggling, try to invert like values. i.e. 1 => 0, etc.
791     case SC.TOGGLE_BEHAVIOR:
792       var sel = this.get('isSelected') ;
793       if (sel) {
794         this.set('value', this.get('toggleOffValue')) ;
795       } else {
796         this.set('value', this.get('toggleOnValue')) ;
797       }
798       break ;
799 
800     // set value to on.  change 0 => 1.
801     case SC.TOGGLE_ON_BEHAVIOR:
802       this.set('value', this.get('toggleOnValue')) ;
803       break ;
804 
805     // set the value to false. change 1 => 0
806     case SC.TOGGLE_OFF_BEHAVIOR:
807       this.set('value', this.get('toggleOffValue')) ;
808       break ;
809 
810     case SC.HOLD_BEHAVIOR:
811       this._runHoldAction(evt, skipHoldRepeat);
812       break ;
813 
814     // otherwise, just trigger an action if there is one.
815     default:
816       //if (this.action) this.action(evt);
817       this._runAction(evt);
818     }
819   },
820 
821   /** @private */
822   _runAction: function(evt) {
823     var action = this.get('action');
824 
825     if (action) {
826       // Legacy support for action functions.
827       if (action && (SC.typeOf(action) === SC.T_FUNCTION)) {
828         this.action(evt);
829 
830       // Use SC.ActionSupport.
831       } else {
832         this.fireAction();
833       }
834     }
835   },
836 
837   /** @private */
838   _runHoldAction: function(evt, skipRepeat) {
839     if (this.get('isActive')) {
840       this._runAction();
841 
842       if (!skipRepeat) {
843         // This run loop appears to only be necessary for testing
844         SC.RunLoop.begin();
845         this.invokeLater('_runHoldAction', this.get('holdInterval'), evt);
846         SC.RunLoop.end();
847       }
848     }
849   },
850 
851 
852   /** @private */
853   didBecomeKeyResponderFrom: function(keyView) {
854     // focus the text field.
855     if (!this._isFocused) {
856       this._isFocused = YES ;
857       this.becomeFirstResponder();
858       if (this.get('isVisibleInWindow')) {
859         this.$().focus();
860       }
861     }
862   },
863 
864   /** @private */
865   willLoseKeyResponderTo: function(responder) {
866     if (this._isFocused) this._isFocused = NO ;
867   },
868 
869   /** @private */
870   didAppendToDocument: function() {
871     if(SC.browser.isIE &&
872         SC.browser.compare(SC.browser.version, '7') === 0 &&
873         this.get('useStaticLayout')){
874       var layout = this.get('layout'),
875           elem = this.$(), w=0;
876       if(elem && elem[0] && (w=elem[0].clientWidth) && w!==0 && this._labelMinWidthIE7===0){
877         var label = this.$('.sc-button-label'),
878             paddingRight = parseInt(label.css('paddingRight'),0),
879             paddingLeft = parseInt(label.css('paddingLeft'),0),
880             marginRight = parseInt(label.css('marginRight'),0),
881             marginLeft = parseInt(label.css('marginLeft'),0);
882         if(marginRight=='auto') SC.Logger.log(marginRight+","+marginLeft+","+paddingRight+","+paddingLeft);
883         if(!paddingRight && isNaN(paddingRight)) paddingRight = 0;
884         if(!paddingLeft && isNaN(paddingLeft)) paddingLeft = 0;
885         if(!marginRight && isNaN(marginRight)) marginRight = 0;
886         if(!marginLeft && isNaN(marginLeft)) marginLeft = 0;
887 
888         this._labelMinWidthIE7 = w-(paddingRight + paddingLeft)-(marginRight + marginLeft);
889         label.css('minWidth', this._labelMinWidthIE7+'px');
890       }else{
891         this.invokeLater(this.didAppendToDocument, 1);
892       }
893     }
894   }
895 
896 }) ;
897 
898 /**
899   How long to wait before triggering the action.
900 
901   @constant
902   @type {Number}
903 */
904 SC.ButtonView.TRIGGER_DELAY = 200;
905 
906 /**
907   The delay after which "click" behavior should transition to "click and hold"
908   behavior. This is used by subclasses such as PopupButtonView and
909   SelectButtonView.
910 
911   @constant
912   @type Number
913 */
914 SC.ButtonView.CLICK_AND_HOLD_DELAY = SC.browser.isIE ? 600 : 300;
915 
916 SC.REGULAR_BUTTON_HEIGHT=24;
917 
918 
919