1// Copyright (c) 2011 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5cr.define('cr.ui', function() {
6  /**
7   * Creates a new button element. The repeating button behaves like a
8   * keyboard button, which auto-repeats if held. This button is designed
9   * for use with controls such as brightness and volume adjustment buttons.
10   * @constructor
11   * @extends {HTMLButtonElement}
12   */
13  var RepeatingButton = cr.ui.define('button');
14
15  /**
16   * DOM Events that may be fired by the Repeating button.
17   */
18  RepeatingButton.Event = {
19    BUTTON_HELD: 'buttonHeld'
20  };
21
22  RepeatingButton.prototype = {
23    __proto__: HTMLButtonElement.prototype,
24
25    /**
26     * Delay in milliseconds before the first repeat trigger of the button
27     * held action.
28     * @type {number}
29     * @private
30     */
31    holdDelayTime_: 500,
32
33    /**
34     * Delay in milliseconds between triggers of the button held action.
35     * @type {number}
36     * @private
37     */
38    holdRepeatIntervalTime_: 50,
39
40    /**
41     * Callback function when repeated intervals trigger. Initialized when the
42     * button is held for an initial delay period and cleared when the button
43     * is released.
44     * @type {function}
45     * @private
46     */
47    intervalCallback_: undefined,
48
49    /**
50     * Callback function to arm the repeat timer. Initialized when the button
51     * is pressed and cleared when the interval timer is set or the button is
52     * released.
53     * @type {function}
54     * @private
55     */
56    armRepeaterCallback_: undefined,
57
58    /**
59     * Initializes the button.
60     */
61    decorate: function() {
62      this.addEventListener('mousedown', this.buttonDown_.bind(this));
63      this.addEventListener('mouseup', this.buttonUp_.bind(this));
64      this.addEventListener('mouseout', this.buttonUp_.bind(this));
65      this.addEventListener('touchstart', this.touchStart_.bind(this));
66      this.addEventListener('touchend', this.buttonUp_.bind(this));
67      this.addEventListener('touchcancel', this.buttonUp_.bind(this));
68    },
69
70    /**
71     * Called when the user initiates a touch gesture.
72     * @param {!Event} e The triggered event.
73     * @private
74     */
75    touchStart_: function(e) {
76      // Block system level gestures to prevent double tap to zoom. Also,
77      // block following mouse event to prevent double firing of the button
78      // held action in the case of a tap. Otherwise, a single tap action in
79      // webkit generates the following event sequence: touchstart, touchend,
80      // mouseover, mousemove, mousedown, mouseup and click.
81      e.preventDefault();
82      this.buttonDown_(e);
83    },
84
85    /**
86     * Called when the user presses this button.
87     * @param {!Event} e The triggered event.
88     * @private
89     */
90    buttonDown_: function(e) {
91      this.clearTimeout_();
92      // Trigger the button held action immediately, after an initial delay and
93      // then repeated based on a fixed time increment. The time intervals are
94      // in agreement with the defaults for the ChromeOS keyboard and virtual
95      // keyboard.
96      // TODO(kevers): Consider adding a common location for picking up the
97      //               initial delay and repeat interval.
98      this.buttonHeld_();
99      var self = this;
100      this.armRepeaterCallback_ = function() {
101        // In the event of a click/tap operation, this button has already been
102        // released by the time this timeout triggers. Test to ensure that the
103        // button is still being held (i.e. clearTimeout has not been called).
104        if (self.armRepeaterCallback_) {
105          self.armRepeaterCallback_ = undefined;
106          self.buttonHeld_();
107          self.intervalCallback_ = setInterval(self.buttonHeld_.bind(self),
108                                               self.holdRepeatIntervalTime_);
109        }
110      };
111      setTimeout(this.armRepeaterCallback_, this.holdDelayTime_);
112    },
113
114    /**
115     * Called when the user releases this button.
116     * @param {!Event} e The triggered event.
117     * @private
118     */
119    buttonUp_: function(e) {
120      this.clearTimeout_();
121    },
122
123    /**
124     * Resets the interval callback.
125     * @private
126     */
127    clearTimeout_: function() {
128      if (this.armRepeaterCallback_) {
129        clearTimeout(this.armRepeaterCallback_);
130        this.armRepeaterCallback_ = undefined;
131      }
132      if (this.intervalCallback_) {
133        clearInterval(this.intervalCallback_);
134        this.intervalCallback_ = undefined;
135      }
136    },
137
138    /**
139     * Dispatches the action associated with keeping this button pressed.
140     * @private
141     */
142    buttonHeld_: function() {
143      cr.dispatchSimpleEvent(this, RepeatingButton.Event.BUTTON_HELD);
144    },
145
146    /**
147     * Getter for the initial delay before repeating.
148     * @type {number} The delay in milliseconds.
149     */
150    get repeatDelay() {
151      return this.holdDelayTime_;
152    },
153
154    /**
155     * Setter for the initial delay before repeating.
156     * @type {number} The delay in milliseconds.
157     */
158    set repeatDelay(delay) {
159      this.holdDelayTime_ = delay;
160    },
161
162    /**
163     * Getter for the repeat interval.
164     * @type {number} The repeat interval in milliseconds.
165     */
166    get repeatInterval() {
167      return this.holdRepeatIntervalTime_;
168    },
169
170    /**
171     * Setter for the repeat interval.
172     * @type {number} The interval in milliseconds.
173     */
174   set repeatInterval(delay) {
175     this.holdRepeatIntervalTime_ = delay;
176   }
177  };
178
179  return {
180    RepeatingButton: RepeatingButton
181  };
182});
183
184