OLD | NEW |
| (Empty) |
1 // Copyright 2011 (c) The Native Client 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 | |
5 /** | |
6 * @file | |
7 * A slider control. | |
8 */ | |
9 | |
10 goog.provide('Slider'); | |
11 goog.provide('Slider.Orientation'); | |
12 goog.provide('Slider.SliderEvents'); | |
13 | |
14 goog.require('goog.Disposable'); | |
15 goog.require('goog.dom'); | |
16 goog.require('goog.events'); | |
17 goog.require('goog.events.EventTarget'); | |
18 goog.require('goog.events.EventType'); | |
19 goog.require('goog.style'); | |
20 | |
21 /** | |
22 * The slider control. | |
23 * @param {!Array} stepObjects An array of objects, each object is associated | |
24 * with the step at the object's index in the array. The object is | |
25 * returned by objectAtStepValue(). | |
26 * @param {?string} opt_orientation The orentation, default is HORIZONTAL. | |
27 * @constructor | |
28 * @extends {goog.EventTarget} | |
29 */ | |
30 Slider = function(stepObjects, opt_orientation) { | |
31 goog.events.EventTarget.call(this); | |
32 | |
33 /** | |
34 * Cached DOM elements for the slider parts. | |
35 * @type {Element} | |
36 * @private | |
37 */ | |
38 this.container_ = null; // The slider's outer container. | |
39 this.ruler_ = null; // The slider's ruler element. | |
40 this.thumb_ = null; // The slider's thumb. | |
41 | |
42 /** | |
43 * The current value. In a discreet slider, this has to be an even multiple | |
44 * of |this.stepSize_|. | |
45 */ | |
46 this.currentValue_ = 0; | |
47 | |
48 /** | |
49 * The number of steps between the start and end point (not including the | |
50 * end points). | |
51 * @type {number} | |
52 * @private | |
53 */ | |
54 this.stepCount_ = stepObjects.length; | |
55 | |
56 /** | |
57 * The step objects. | |
58 * @type {Array} | |
59 * @private | |
60 */ | |
61 this.stepObjects_ = stepObjects; | |
62 | |
63 /** | |
64 * The orientation. 'h' means horizontal (the default); 'v' means vertical. | |
65 * @type {string} | |
66 * @private | |
67 */ | |
68 this.orientation_ = opt_orientation || Slider.Orientation.HORIZONTAL; | |
69 | |
70 /** | |
71 * Indicates whether the thumb is being dragged. | |
72 * @type {boolean} | |
73 * @private | |
74 */ | |
75 this.isDragging_ = false; | |
76 | |
77 /** | |
78 * Event handlers. These are the keys returned by goog.events.listen(). | |
79 * @type {number} | |
80 * @private | |
81 */ | |
82 this.rulerClickListener_ = null; | |
83 this.thumbMouseDownListener_ = null; | |
84 this.thumbClickListener_ = null; | |
85 this.thumbMouseMoveListener_ = null; | |
86 this.thumbMouseUpListener_ = null; | |
87 | |
88 /** | |
89 * The offset and length of the ruler. Measured in pixels. | |
90 * @type {number} | |
91 */ | |
92 this.rulerLength = 0; | |
93 this.rulerOffset = 0; | |
94 } | |
95 goog.inherits(Slider, goog.events.EventTarget); | |
96 | |
97 /** | |
98 * The slider's orientation: vertical or hoirizontal. | |
99 * @enum {string} | |
100 */ | |
101 Slider.Orientation = { | |
102 HORIZONTAL: 'h', | |
103 VERTICAL: 'v' | |
104 } | |
105 | |
106 /** | |
107 * Override of disposeInternal() to dispose of retained objects and unhook all | |
108 * events. | |
109 * @override | |
110 */ | |
111 Slider.prototype.disposeInternal = function() { | |
112 this.unlisten_(); | |
113 this.container_ = null; | |
114 this.ruler_ = null; | |
115 this.thumb_ = null; | |
116 Slider.superClass_.disposeInternal.call(this); | |
117 } | |
118 | |
119 /** | |
120 * Attach the DOM elements that make up the slider. Wires up all the event | |
121 * listeners. | |
122 * @param {!Element} container The DOM element containing the entire slider. | |
123 * @param {!Element} ruler The DOM element containing the slider's ruler. | |
124 * @param {!Element} thumb The DOM elelemnt representing the slider's thumb. | |
125 */ | |
126 Slider.prototype.decorate = function(container, ruler, thumb) { | |
127 this.unlisten_(); | |
128 this.container_ = container; // The slider's outer container. | |
129 this.ruler_ = ruler; // The slider's ruler element. | |
130 this.thumb_ = thumb; // The slider's thumb. | |
131 | |
132 // Establish a default ruler length from the |ruler| element. | |
133 var rulerSize = goog.style.getSize(this.ruler_) | |
134 if (this.isHorizontal()) { | |
135 this.rulerLength = rulerSize.width; | |
136 } else { | |
137 this.rulerLength = rulerSize.height; | |
138 } | |
139 | |
140 // Wire up the mouse event handlers. | |
141 this.rulerClickListener_ = goog.events.listen( | |
142 this.ruler_, | |
143 goog.events.EventType.CLICK, | |
144 this.rulerClick_, true, this); | |
145 this.thumbMouseDownListener_ = goog.events.listen( | |
146 this.thumb_, | |
147 goog.events.EventType.MOUSEDOWN, | |
148 this.thumbMouseDown_, true, this); | |
149 this.thumbClickListener_ = goog.events.listen( | |
150 this.thumb_, | |
151 goog.events.EventType.CLICK, | |
152 function(e) { | |
153 e.stopPropagation(); | |
154 }, true, this); | |
155 this.thumbMouseMoveListener_ = null; | |
156 this.thumbMouseUpListener_ = null; | |
157 | |
158 // Complete initialization. | |
159 this.slideToValue(this.currentValue_); | |
160 } | |
161 | |
162 /** | |
163 * The slider's continuous value. | |
164 * @return {number} the value. | |
165 */ | |
166 Slider.prototype.value = function() { | |
167 return this.currentValue_; | |
168 } | |
169 | |
170 /** | |
171 * The slider's discreet value as a step index. | |
172 * @return {number} the value. | |
173 */ | |
174 Slider.prototype.stepValue = function() { | |
175 return Math.round(this.currentValue_ / this.stepSize_()); | |
176 } | |
177 | |
178 /** | |
179 * The object at a step value. | |
180 * @param {number} opt_step The step index (0-based). Default is to use the | |
181 * current step value. | |
182 * @return {number} the value. | |
183 */ | |
184 Slider.prototype.objectAtStepValue = function(opt_step) { | |
185 var stepValue = opt_step || this.stepValue(); | |
186 return this.stepObjects_[stepValue]; | |
187 } | |
188 | |
189 /** | |
190 * Is the slider horizontal? | |
191 * @return {boolean} |true| if the slider is horizontal. | |
192 */ | |
193 Slider.prototype.isHorizontal = function() { | |
194 return this.orientation_ == Slider.Orientation.HORIZONTAL; | |
195 } | |
196 | |
197 /** | |
198 * Slide the thumb along the slider so that it represents |value|. If the | |
199 * slider has no length (its step count is 0), then do nothing. | |
200 * @param {number} value The new value of the thumb. Measured in pixels from | |
201 * the left edge of the slider's container. | |
202 */ | |
203 Slider.prototype.slideToValue = function(value) { | |
204 if (this.stepCount_ == 0) { | |
205 return; | |
206 } | |
207 | |
208 // Validate and clip |value| to the range of the slider. | |
209 if (value < 0) { | |
210 value = 0; | |
211 } | |
212 if (value >= this.rulerLength) { | |
213 value = this.rulerLength; | |
214 } | |
215 var stepSize = this.stepSize_(); | |
216 var stepValue = Math.round(value / stepSize); | |
217 value = stepValue * stepSize; | |
218 if (this.isHorizontal()) { | |
219 this.thumb_.style.left = value + this.rulerOffset + 'px'; | |
220 } else { | |
221 this.thumb_.style.top = value + this.rulerOffset + 'px'; | |
222 } | |
223 this.currentValue_ = value; | |
224 } | |
225 | |
226 /** | |
227 * Slide to a step value. The step value is a 0-based index into the discreet | |
228 * steps. | |
229 * @param {number} step The step value. | |
230 */ | |
231 Slider.prototype.slideToStep = function(step) { | |
232 this.slideToValue(step * this.stepSize_()); | |
233 } | |
234 | |
235 /** | |
236 * The step size. In a discreet slider, the thumb can only be an even | |
237 * multiple of this value. Measured in pixels. Never returns 0. | |
238 * @return {number} | |
239 * @private | |
240 */ | |
241 Slider.prototype.stepSize_ = function() { | |
242 if (this.stepCount_ > 1) { | |
243 return this.rulerLength / (this.stepCount_ - 1); | |
244 } | |
245 return 1; | |
246 } | |
247 | |
248 /** | |
249 * Remove all event listenetrs from any cached DOM elements. | |
250 * @private | |
251 */ | |
252 Slider.prototype.unlisten_ = function() { | |
253 function removeAllEvents(element) { | |
254 if (element) { | |
255 goog.events.removeAll(this.container_); | |
256 } | |
257 } | |
258 removeAllEvents(this.container_); | |
259 removeAllEvents(this.ruler_); | |
260 removeAllEvents(this.thumb_); | |
261 this.rulerClickListener_ = null; | |
262 this.thumbMouseDownListener_ = null; | |
263 this.thumbClickListener_ = null; | |
264 this.thumbMouseMoveListener_ = null; | |
265 this.thumbMouseUpListener_ = null; | |
266 } | |
267 | |
268 /** | |
269 * Handle a 'click' event on the ruler itself. This slides the thumb to the | |
270 * closets tick to where the mouse click happened. | |
271 * @param {Event} clickEvent The event that triggered this handler. | |
272 */ | |
273 Slider.prototype.rulerClick_ = function(clickEvent) { | |
274 if (!this.isDragging_) { | |
275 this.slideToValue(this.rulerOffsetFromEvent_(clickEvent)); | |
276 this.dispatchEvent(new SliderEvent(this.value(), this.stepValue())); | |
277 } | |
278 } | |
279 | |
280 /** | |
281 * Handle a mousedown on the thumb. This starts the thumb-drag state. | |
282 * @param {Event} mouseEvent The event that triggered this handler. | |
283 */ | |
284 Slider.prototype.thumbMouseDown_ = function(mouseEvent) { | |
285 if (this.isDragging_) { | |
286 return; | |
287 } | |
288 mouseEvent.preventDefault(); | |
289 mouseEvent.stopPropagation(); | |
290 this.isDragging_ = true; | |
291 // Listen for mouse-move and mouse-up events. Dragging stops when the | |
292 // mouse-up event arrives. | |
293 if (this.thumbMouseMoveListener_ == null) { | |
294 this.thumbMouseMoveListener_ = goog.events.listen( | |
295 this.container_, | |
296 goog.events.EventType.MOUSEMOVE, | |
297 this.thumbMouseMove_, true, this); | |
298 } | |
299 if (this.thumbMouseUpListener_ == null) { | |
300 this.thumbMouseUpListener_ = goog.events.listen( | |
301 window, | |
302 goog.events.EventType.MOUSEUP, | |
303 this.thumbMouseUp_, true, this); | |
304 } | |
305 } | |
306 | |
307 /** | |
308 * Handle a mousemove on the thumb. Drag thte thumb to the new value. | |
309 * @param {Event} mouseEvent The event that triggered this handler. | |
310 */ | |
311 Slider.prototype.thumbMouseMove_ = function(mouseEvent) { | |
312 this.slideToValue(this.rulerOffsetFromEvent_(mouseEvent)); | |
313 } | |
314 | |
315 /** | |
316 * Handle a mouseup on the thumb. Leave the drag state, post a change event. | |
317 * @param {Event} mouseEvent The event that triggered this handler. | |
318 */ | |
319 Slider.prototype.thumbMouseUp_ = function(mouseEvent) { | |
320 if (this.thumbMouseMoveListener_ != null) { | |
321 goog.events.unlistenByKey(this.thumbMouseMoveListener_); | |
322 this.thumbMouseMoveListener_ = null; | |
323 } | |
324 if (this.thumbMouseUpListener_ != null) { | |
325 goog.events.unlistenByKey(this.thumbMouseUpListener_); | |
326 this.thumbMouseUpListener_ = null; | |
327 } | |
328 this.dispatchEvent(new SliderEvent(this.value(), this.stepValue())); | |
329 this.isDragging_ = false; | |
330 } | |
331 | |
332 /** | |
333 * Compute the pixel offset of an event from the ruler's origin. | |
334 * @param {BrowserEvent} event A BrowserEvent that has clientX and clientY. | |
335 * @return {number} A one-dimensional pixel offset of the event. | |
336 */ | |
337 Slider.prototype.rulerOffsetFromEvent_ = function(event) { | |
338 var rulerOffset = 0; | |
339 if (this.isHorizontal()) { | |
340 var rulerOrigin = goog.style.getPageOffsetLeft(this.ruler_); | |
341 rulerOffset = event.clientX - rulerOrigin; | |
342 } else { | |
343 var rulerOrigin = goog.style.getPageOffsetTop(this.ruler_); | |
344 rulerOffset = event.clientY - rulerOrigin; | |
345 } | |
346 return rulerOffset - this.rulerOffset; | |
347 } | |
348 | |
OLD | NEW |