Skip to content

Commit 7115e89

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

File tree

5 files changed

+145
-17
lines changed

5 files changed

+145
-17
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: 103 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@ import {
77
Input,
88
Output,
99
EventEmitter,
10-
AfterContentInit
10+
AfterContentInit,
11+
ViewChild
1112
} from '@angular/core';
1213
import {
1314
ControlValueAccessor,
1415
NG_VALUE_ACCESSOR
1516
} from '@angular/forms';
16-
import { BooleanFieldValue } from '@angular2-material/core/annotations/field-value';
17-
import { Observable } from 'rxjs/Observable';
17+
import {BooleanFieldValue} from '@angular2-material/core/annotations/field-value';
18+
import {Observable} from 'rxjs/Observable';
19+
import {applyCssTransform} from '@angular2-material/core/style/apply-transform';
1820

1921
export const MD_SLIDE_TOGGLE_VALUE_ACCESSOR: any = {
2022
provide: NG_VALUE_ACCESSOR,
@@ -58,6 +60,13 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
5860
private _hasFocus: boolean = false;
5961
private _isMousedown: boolean = false;
6062
private _isInitialized: boolean = false;
63+
private _domRenderer: MdSlideToggleRenderer = null;
64+
65+
// Drag pointer, which holds information about the current drag.
66+
private _dragPointer: {
67+
barWidth: number;
68+
percentage?: number;
69+
};
6170

6271
@Input() @BooleanFieldValue() disabled: boolean = false;
6372
@Input() name: string = null;
@@ -74,6 +83,7 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
7483

7584
constructor(private _elementRef: ElementRef,
7685
private _renderer: Renderer) {
86+
this._domRenderer = new MdSlideToggleRenderer(this._elementRef);
7787
}
7888

7989
/** TODO: internal */
@@ -95,7 +105,8 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
95105
// emit its event object to the component's `change` output.
96106
event.stopPropagation();
97107

98-
if (!this.disabled) {
108+
// Once a drag is currently in progress, we do not want to toggle the slide-toggle on a click.
109+
if (!this.disabled && !this._dragPointer) {
99110
this.toggle();
100111
}
101112
}
@@ -202,13 +213,101 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
202213
}
203214
}
204215

216+
/** Emits the change event to the `change` output EventEmitter */
205217
private _emitChangeEvent() {
206218
let event = new MdSlideToggleChange();
207219
event.source = this;
208220
event.checked = this.checked;
209221
this._change.emit(event);
210222
}
211223

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

214313
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)