@@ -12,6 +12,12 @@ import {BooleanFieldValue} from '@angular2-material/core/annotations/field-value
12
12
import { applyCssTransform } from '@angular2-material/core/style/apply-transform' ;
13
13
import { MdGestureConfig } from '@angular2-material/core/core' ;
14
14
15
+ /**
16
+ * Visually, a 30px separation between tick marks looks best. This is very subjective but it is
17
+ * the default separation we chose.
18
+ */
19
+ const MIN_AUTO_TICK_SEPARATION = 30 ;
20
+
15
21
@Component ( {
16
22
moduleId : module . id ,
17
23
selector : 'md-slider' ,
@@ -53,6 +59,12 @@ export class MdSlider implements AfterContentInit {
53
59
/** The values at which the thumb will snap. */
54
60
@Input ( ) step : number = 1 ;
55
61
62
+ /**
63
+ * How often to show ticks. Relative to the step so that a tick always appears on a step.
64
+ * Ex: Tick interval of 4 with a step of 3 will draw a tick every 4 steps (every 12 values).
65
+ */
66
+ @Input ( 'tick-interval' ) private _tickInterval : 'auto' | number ;
67
+
56
68
/**
57
69
* Whether or not the thumb is sliding.
58
70
* Used to determine if there should be a transition for the thumb and fill track.
@@ -122,6 +134,7 @@ export class MdSlider implements AfterContentInit {
122
134
ngAfterContentInit ( ) {
123
135
this . _sliderDimensions = this . _renderer . getSliderDimensions ( ) ;
124
136
this . snapToValue ( ) ;
137
+ this . _updateTickSeparation ( ) ;
125
138
}
126
139
127
140
/** TODO: internal */
@@ -186,7 +199,7 @@ export class MdSlider implements AfterContentInit {
186
199
* This is also used to move the thumb to a snapped value once sliding is done.
187
200
*/
188
201
updatePercentFromValue ( ) {
189
- this . _percent = ( this . value - this . min ) / ( this . max - this . min ) ;
202
+ this . _percent = this . calculatePercentage ( this . value ) ;
190
203
}
191
204
192
205
/**
@@ -198,7 +211,7 @@ export class MdSlider implements AfterContentInit {
198
211
199
212
// The exact value is calculated from the event and used to find the closest snap value.
200
213
this . _percent = this . clamp ( ( pos - offset ) / size ) ;
201
- let exactValue = this . min + ( this . _percent * ( this . max - this . min ) ) ;
214
+ let exactValue = this . calculateValue ( this . _percent ) ;
202
215
203
216
// This calculation finds the closest step by finding the closest whole number divisible by the
204
217
// step relative to the min.
@@ -217,6 +230,80 @@ export class MdSlider implements AfterContentInit {
217
230
this . _renderer . updateThumbAndFillPosition ( this . _percent , this . _sliderDimensions . width ) ;
218
231
}
219
232
233
+ /**
234
+ * Calculates the separation in pixels of tick marks. If there is no tick interval or the interval
235
+ * is set to something other than a number or 'auto', nothing happens.
236
+ */
237
+ private _updateTickSeparation ( ) {
238
+ if ( this . _tickInterval == 'auto' ) {
239
+ this . _updateAutoTickSeparation ( ) ;
240
+ } else if ( Number ( this . _tickInterval ) ) {
241
+ this . _updateTickSeparationFromInterval ( ) ;
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Calculates the optimal separation in pixels of tick marks based on the minimum auto tick
247
+ * separation constant.
248
+ */
249
+ private _updateAutoTickSeparation ( ) {
250
+ // We're looking for the multiple of step for which the separation between is greater than the
251
+ // minimum tick separation.
252
+ let sliderWidth = this . _sliderDimensions . width ;
253
+
254
+ // This is the total "width" of the slider in terms of values.
255
+ let valueWidth = this . max - this . min ;
256
+
257
+ // Calculate how many values exist within 1px on the slider.
258
+ let valuePerPixel = valueWidth / sliderWidth ;
259
+
260
+ // Calculate how many values exist in the minimum tick separation (px).
261
+ let valuePerSeparation = valuePerPixel * MIN_AUTO_TICK_SEPARATION ;
262
+
263
+ // Calculate how many steps exist in this separation. This will be the lowest value you can
264
+ // multiply step by to get a separation that is greater than or equal to the minimum tick
265
+ // separation.
266
+ let stepsPerSeparation = Math . ceil ( valuePerSeparation / this . step ) ;
267
+
268
+ // Get the percentage of the slider for which this tick would be located so we can then draw
269
+ // it on the slider.
270
+ let tickPercentage = this . calculatePercentage ( ( this . step * stepsPerSeparation ) + this . min ) ;
271
+
272
+ // The pixel value of the tick is the percentage * the width of the slider. Use this to draw
273
+ // the ticks on the slider.
274
+ this . _renderer . drawTicks ( sliderWidth * tickPercentage ) ;
275
+ }
276
+
277
+ /**
278
+ * Calculates the separation of tick marks by finding the pixel value of the tickInterval.
279
+ */
280
+ private _updateTickSeparationFromInterval ( ) {
281
+ // Force tickInterval to be a number so it can be used in calculations.
282
+ let interval : number = < number > this . _tickInterval ;
283
+ // Calculate the first value a tick will be located at by getting the step at which the interval
284
+ // lands and adding that to the min.
285
+ let tickValue = ( this . step * interval ) + this . min ;
286
+
287
+ // The percentage of the step on the slider is needed in order to calculate the pixel offset
288
+ // from the beginning of the slider. This offset is the tick separation.
289
+ let tickPercentage = this . calculatePercentage ( tickValue ) ;
290
+ this . _renderer . drawTicks ( this . _sliderDimensions . width * tickPercentage ) ;
291
+ }
292
+
293
+ /**
294
+ * Calculates the percentage of the slider that a value is.
295
+ */
296
+ calculatePercentage ( value : number ) {
297
+ return ( value - this . min ) / ( this . max - this . min ) ;
298
+ }
299
+
300
+ /**
301
+ * Calculates the value a percentage of the slider corresponds to.
302
+ */
303
+ calculateValue ( percentage : number ) {
304
+ return this . min + ( percentage * ( this . max - this . min ) ) ;
305
+ }
306
+
220
307
/**
221
308
* Return a number between two numbers.
222
309
*/
@@ -267,6 +354,32 @@ export class SliderRenderer {
267
354
addFocus ( ) {
268
355
this . _sliderElement . focus ( ) ;
269
356
}
357
+
358
+ /**
359
+ * Draws ticks onto the tick container.
360
+ */
361
+ drawTicks ( tickSeparation : number ) {
362
+ let tickContainer = < HTMLElement > this . _sliderElement . querySelector ( '.md-slider-tick-container' ) ;
363
+ let tickContainerWidth = tickContainer . getBoundingClientRect ( ) . width ;
364
+ // An extra element for the last tick is needed because the linear gradient cannot be told to
365
+ // always draw a tick at the end of the gradient. To get around this, there is a second
366
+ // container for ticks that has a single tick mark on the very right edge.
367
+ let lastTickContainer =
368
+ < HTMLElement > this . _sliderElement . querySelector ( '.md-slider-last-tick-container' ) ;
369
+ // Subtract 1 from the tick separation to center the tick.
370
+ // TODO: Evaluate the rendering performance of using repeating background gradients.
371
+ tickContainer . style . background = `repeating-linear-gradient(to right, black, black 2px, ` +
372
+ `transparent 2px, transparent ${ tickSeparation - 1 } px)` ;
373
+ // Add a tick to the very end by starting on the right side and adding a 2px black line.
374
+ lastTickContainer . style . background = `linear-gradient(to left, black, black 2px, transparent ` +
375
+ `2px, transparent)` ;
376
+
377
+ // If the second to last tick is too close (a separation of less than half the normal
378
+ // separation), don't show it by decreasing the width of the tick container element.
379
+ if ( tickContainerWidth % tickSeparation < ( tickSeparation / 2 ) ) {
380
+ tickContainer . style . width = tickContainerWidth - tickSeparation + 'px' ;
381
+ }
382
+ }
270
383
}
271
384
272
385
export const MD_SLIDER_DIRECTIVES = [ MdSlider ] ;
0 commit comments