Skip to content

Commit 226dc3e

Browse files
committed
feat(slide-toggle): add drag functionality to thumb
1 parent 4bb7790 commit 226dc3e

File tree

5 files changed

+143
-16
lines changed

5 files changed

+143
-16
lines changed

src/components/slide-toggle/slide-toggle.html

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
<label class="md-slide-toggle-label">
2+
23
<div class="md-slide-toggle-container">
34
<div class="md-slide-toggle-bar"></div>
4-
<div class="md-slide-toggle-thumb-container">
5+
6+
<div class="md-slide-toggle-thumb-container"
7+
(slidestart)="_onDragStart($event)"
8+
(slide)="_onDrag($event)"
9+
(slideend)="_onDragEnd($event)">
10+
511
<div class="md-slide-toggle-thumb">
612
<div class="md-ink-ripple"></div>
713
</div>

src/components/slide-toggle/slide-toggle.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,12 @@ $md-slide-toggle-margin: 16px !default;
129129

130130
transition: $swift-linear;
131131
transition-property: transform;
132+
133+
// Once the thumb container is being dragged around, we remove the transition duration to
134+
// make the drag feeling fast and not delayed.
135+
&.md-dragging {
136+
transition-duration: 0ms;
137+
}
132138
}
133139

134140
// The thumb will be elevated from the slide-toggle bar.

src/components/slide-toggle/slide-toggle.ts

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ import {
1313
ControlValueAccessor,
1414
NG_VALUE_ACCESSOR
1515
} from '@angular/forms';
16-
import { BooleanFieldValue } from '@angular2-material/core/annotations/field-value';
17-
import { Observable } from 'rxjs/Observable';
16+
import {BooleanFieldValue} from '@angular2-material/core/annotations/field-value';
17+
import {Observable} from 'rxjs/Observable';
18+
import {applyCssTransform} from '@angular2-material/core/style/apply-transform';
1819

1920
export const MD_SLIDE_TOGGLE_VALUE_ACCESSOR: any = {
2021
provide: NG_VALUE_ACCESSOR,
@@ -58,6 +59,13 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
5859
private _hasFocus: boolean = false;
5960
private _isMousedown: boolean = false;
6061
private _isInitialized: boolean = false;
62+
private _slideRenderer: MdSlideToggleRenderer = null;
63+
64+
// State of the current drag, which holds required variables for the drag.
65+
private _dragState: {
66+
barWidth: number;
67+
percentage?: number;
68+
};
6169

6270
@Input() @BooleanFieldValue() disabled: boolean = false;
6371
@Input() name: string = null;
@@ -74,6 +82,7 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
7482

7583
constructor(private _elementRef: ElementRef,
7684
private _renderer: Renderer) {
85+
this._slideRenderer = new MdSlideToggleRenderer(this._elementRef);
7786
}
7887

7988
/** TODO: internal */
@@ -95,7 +104,8 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
95104
// emit its event object to the component's `change` output.
96105
event.stopPropagation();
97106

98-
if (!this.disabled) {
107+
// Once a drag is currently in progress, we do not want to toggle the slide-toggle on a click.
108+
if (!this.disabled && !this._dragState) {
99109
this.toggle();
100110
}
101111
}
@@ -202,13 +212,101 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
202212
}
203213
}
204214

215+
/** Emits the change event to the `change` output EventEmitter */
205216
private _emitChangeEvent() {
206217
let event = new MdSlideToggleChange();
207218
event.source = this;
208219
event.checked = this.checked;
209220
this._change.emit(event);
210221
}
211222

223+
224+
/** TODO: internal */
225+
_onDragStart() {
226+
if (this._dragState) {
227+
return;
228+
}
229+
230+
let thumbBarRect = this._slideRenderer.getThumbBarClientRect();
231+
let thumbRect = this._slideRenderer.getThumbClientRect();
232+
233+
this._dragState = {
234+
barWidth: thumbBarRect.width - thumbRect.width
235+
};
236+
237+
this._slideRenderer.toggleDragging(true);
238+
}
239+
240+
/** TODO: internal */
241+
_onDrag(event: HammerInput) {
242+
if (!this._dragState) {
243+
return;
244+
}
245+
246+
let percentage = (event.deltaX / this._dragState.barWidth) * 100;
247+
248+
// When the slide-toggle was initially checked, then we have to start the drag at 100%
249+
if (this.checked) {
250+
percentage += 100;
251+
}
252+
253+
percentage = Math.max(0, Math.min(percentage, 100));
254+
255+
this._slideRenderer.updateThumbPosition(percentage);
256+
this._dragState.percentage = percentage;
257+
}
258+
259+
/** TODO: internal */
260+
_onDragEnd() {
261+
if (!this._dragState) {
262+
return;
263+
}
264+
265+
this.checked = this._dragState.percentage > 50;
266+
267+
this._slideRenderer.updateThumbPosition(null);
268+
this._slideRenderer.toggleDragging(false);
269+
270+
// We have to clear the drag after one tick, because otherwise
271+
// the click event will fire and toggle the slide-toggle again.
272+
setTimeout(() => { this._dragState = null; }, 0);
273+
}
274+
275+
}
276+
277+
/**
278+
* Renderer for the Slide Toggle component, which separates DOM modification in it's own class
279+
*/
280+
export class MdSlideToggleRenderer {
281+
282+
constructor(private _elementRef: ElementRef) {}
283+
284+
getThumbClientRect(): ClientRect {
285+
let thumbEl = this._elementRef.nativeElement.querySelector('.md-slide-toggle-thumb-container');
286+
return thumbEl.getBoundingClientRect();
287+
}
288+
289+
getThumbBarClientRect(): ClientRect {
290+
let thumbBarEl = this._elementRef.nativeElement.querySelector('.md-slide-toggle-bar');
291+
return thumbBarEl.getBoundingClientRect();
292+
}
293+
294+
/**
295+
* Updates the thumb containers position by using the specified percentage.
296+
* When the percentage is set to `null`, the custom thumb position will be removed.
297+
*/
298+
updateThumbPosition(percentage: number) {
299+
let thumbEl = this._elementRef.nativeElement.querySelector('.md-slide-toggle-thumb-container');
300+
applyCssTransform(thumbEl, percentage === null ? '' : `translate3d(${percentage}%, 0, 0)`);
301+
}
302+
303+
/** Toggles the dragging class for the thumb container to toggle the transition duration. */
304+
toggleDragging(isDragging: boolean) {
305+
let thumbEl = this._elementRef.nativeElement.querySelector('.md-slide-toggle-thumb-container');
306+
thumbEl.classList.toggle('md-dragging', isDragging);
307+
}
308+
309+
212310
}
213311

214312
export const MD_SLIDE_TOGGLE_DIRECTIVES = [MdSlideToggle];

src/core/gestures/MdGestureConfig.ts

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {HammerGestureConfig} from '@angular/platform-browser';
44
/* Adjusts configuration of our gesture library, Hammer. */
55
@Injectable()
66
export class MdGestureConfig extends HammerGestureConfig {
7+
78
/* List of new event names to add to the gesture support list */
89
events: string[] = [
910
'drag',
@@ -12,6 +13,11 @@ export class MdGestureConfig extends HammerGestureConfig {
1213
'dragright',
1314
'dragleft',
1415
'longpress',
16+
'slide',
17+
'slidestart',
18+
'slideend',
19+
'slideright',
20+
'slideleft'
1521
];
1622

1723
/*
@@ -29,22 +35,32 @@ export class MdGestureConfig extends HammerGestureConfig {
2935
buildHammer(element: HTMLElement) {
3036
var mc = new Hammer(element);
3137

32-
// create custom gesture recognizers
33-
var drag = new Hammer.Pan({event: 'drag', threshold: 6});
34-
var longpress = new Hammer.Press({event: 'longpress', time: 500});
38+
// Create custom gesture recognizers
39+
let drag = this._createRecognizer(Hammer.Pan, {event: 'drag', threshold: 6}, Hammer.Swipe);
40+
let slide = this._createRecognizer(Hammer.Pan, {event: 'slide', threshold: 0}, Hammer.Swipe);
41+
let longpress = this._createRecognizer(Hammer.Press, {event: 'longpress', time: 500});
42+
43+
let pan = new Hammer.Pan();
44+
let swipe = new Hammer.Swipe();
3545

36-
// ensure custom recognizers can coexist with the default gestures (i.e. pan, press, swipe)
37-
var pan = new Hammer.Pan();
38-
var press = new Hammer.Press();
39-
var swipe = new Hammer.Swipe();
40-
drag.recognizeWith(pan);
41-
drag.recognizeWith(swipe);
46+
// Overwrite the default `pan` event to use the swipe event.
4247
pan.recognizeWith(swipe);
43-
longpress.recognizeWith(press);
4448

45-
// add customized gestures to Hammer manager
46-
mc.add([drag, pan, swipe, press, longpress]);
49+
// Add customized gestures to Hammer manager
50+
mc.add([drag, slide, pan, longpress]);
51+
4752
return mc;
4853
}
4954

55+
/** Creates a new recognizer, without affecting the default recognizers of HammerJS */
56+
private _createRecognizer(type: RecognizerStatic, options: any, ...extra: RecognizerStatic[]) {
57+
let recognizer = new type(options);
58+
59+
// Add the default recognizer to the new custom recognizer.
60+
extra.push(type);
61+
extra.forEach(entry => recognizer.recognizeWith(new entry()));
62+
63+
return recognizer;
64+
}
65+
5066
}

test/karma.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export function config(config) {
2020
],
2121
files: [
2222
{pattern: 'dist/vendor/core-js/client/core.js', included: true, watched: false},
23+
{pattern: 'dist/vendor/hammerjs/hammer.min.js', included: true, watched: false},
2324
{pattern: 'dist/vendor/systemjs/dist/system-polyfills.js', included: true, watched: false},
2425
{pattern: 'dist/vendor/systemjs/dist/system.src.js', included: true, watched: false},
2526
{pattern: 'dist/vendor/zone.js/dist/zone.js', included: true, watched: false},

0 commit comments

Comments
 (0)