@@ -12,6 +12,12 @@ import {BooleanFieldValue} from '@angular2-material/core/annotations/field-value
1212import { applyCssTransform } from '@angular2-material/core/style/apply-transform' ;
1313import { MdGestureConfig } from '@angular2-material/core/core' ;
1414
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+
1521@Component ( {
1622 moduleId : module . id ,
1723 selector : 'md-slider' ,
@@ -53,6 +59,12 @@ export class MdSlider implements AfterContentInit {
5359 /** The values at which the thumb will snap. */
5460 @Input ( ) step : number = 1 ;
5561
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+
5668 /**
5769 * Whether or not the thumb is sliding.
5870 * Used to determine if there should be a transition for the thumb and fill track.
@@ -122,6 +134,7 @@ export class MdSlider implements AfterContentInit {
122134 ngAfterContentInit ( ) {
123135 this . _sliderDimensions = this . _renderer . getSliderDimensions ( ) ;
124136 this . snapToValue ( ) ;
137+ this . _updateTickSeparation ( ) ;
125138 }
126139
127140 /** TODO: internal */
@@ -186,7 +199,7 @@ export class MdSlider implements AfterContentInit {
186199 * This is also used to move the thumb to a snapped value once sliding is done.
187200 */
188201 updatePercentFromValue ( ) {
189- this . _percent = ( this . value - this . min ) / ( this . max - this . min ) ;
202+ this . _percent = this . calculatePercentage ( this . value ) ;
190203 }
191204
192205 /**
@@ -198,7 +211,7 @@ export class MdSlider implements AfterContentInit {
198211
199212 // The exact value is calculated from the event and used to find the closest snap value.
200213 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 ) ;
202215
203216 // This calculation finds the closest step by finding the closest whole number divisible by the
204217 // step relative to the min.
@@ -217,6 +230,80 @@ export class MdSlider implements AfterContentInit {
217230 this . _renderer . updateThumbAndFillPosition ( this . _percent , this . _sliderDimensions . width ) ;
218231 }
219232
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+
220307 /**
221308 * Return a number between two numbers.
222309 */
@@ -267,6 +354,32 @@ export class SliderRenderer {
267354 addFocus ( ) {
268355 this . _sliderElement . focus ( ) ;
269356 }
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+ }
270383}
271384
272385export const MD_SLIDER_DIRECTIVES = [ MdSlider ] ;
0 commit comments