Skip to content

Commit 25b4f21

Browse files
devversionkara
authored andcommitted
feat(slide-toggle): add drag functionality to thumb (#750)
1 parent 9552ed5 commit 25b4f21

File tree

5 files changed

+136
-19
lines changed

5 files changed

+136
-19
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: 94 additions & 6 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,7 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
5859
private _hasFocus: boolean = false;
5960
private _isMousedown: boolean = false;
6061
private _isInitialized: boolean = false;
62+
private _slideRenderer: SlideToggleRenderer = null;
6163

6264
@Input() @BooleanFieldValue() disabled: boolean = false;
6365
@Input() name: string = null;
@@ -72,12 +74,12 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
7274
// Returns the unique id for the visual hidden input.
7375
getInputId = () => `${this.id || this._uniqueId}-input`;
7476

75-
constructor(private _elementRef: ElementRef,
76-
private _renderer: Renderer) {
77-
}
77+
constructor(private _elementRef: ElementRef, private _renderer: Renderer) {}
7878

7979
/** TODO: internal */
8080
ngAfterContentInit() {
81+
this._slideRenderer = new SlideToggleRenderer(this._elementRef);
82+
8183
// Mark this component as initialized in AfterContentInit because the initial checked value can
8284
// possibly be set by NgModel or the checked attribute. This would cause the change event to
8385
// be emitted, before the component is actually initialized.
@@ -95,7 +97,8 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
9597
// emit its event object to the component's `change` output.
9698
event.stopPropagation();
9799

98-
if (!this.disabled) {
100+
// Once a drag is currently in progress, we do not want to toggle the slide-toggle on a click.
101+
if (!this.disabled && !this._slideRenderer.isDragging()) {
99102
this.toggle();
100103
}
101104
}
@@ -202,13 +205,98 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
202205
}
203206
}
204207

208+
/** Emits the change event to the `change` output EventEmitter */
205209
private _emitChangeEvent() {
206210
let event = new MdSlideToggleChange();
207211
event.source = this;
208212
event.checked = this.checked;
209213
this._change.emit(event);
210214
}
211215

216+
217+
/** TODO: internal */
218+
_onDragStart() {
219+
this._slideRenderer.startThumbDrag(this.checked);
220+
}
221+
222+
/** TODO: internal */
223+
_onDrag(event: HammerInput) {
224+
this._slideRenderer.updateThumbPosition(event.deltaX);
225+
}
226+
227+
/** TODO: internal */
228+
_onDragEnd() {
229+
// Notice that we have to stop outside of the current event handler,
230+
// because otherwise the click event will be fired and will reset the new checked variable.
231+
setTimeout(() => {
232+
this.checked = this._slideRenderer.stopThumbDrag();
233+
}, 0);
234+
}
235+
236+
}
237+
238+
/**
239+
* Renderer for the Slide Toggle component, which separates DOM modification in its own class
240+
*/
241+
class SlideToggleRenderer {
242+
243+
private _thumbEl: HTMLElement;
244+
private _thumbBarEl: HTMLElement;
245+
private _thumbBarWidth: number;
246+
private _checked: boolean;
247+
private _percentage: number;
248+
249+
constructor(private _elementRef: ElementRef) {
250+
this._thumbEl = _elementRef.nativeElement.querySelector('.md-slide-toggle-thumb-container');
251+
this._thumbBarEl = _elementRef.nativeElement.querySelector('.md-slide-toggle-bar');
252+
}
253+
254+
/** Whether the slide-toggle is currently dragging. */
255+
isDragging(): boolean {
256+
return !!this._thumbBarWidth;
257+
}
258+
259+
/** Initializes the drag of the slide-toggle. */
260+
startThumbDrag(checked: boolean) {
261+
if (!this._thumbBarWidth) {
262+
this._thumbBarWidth = this._thumbBarEl.clientWidth - this._thumbEl.clientWidth;
263+
this._checked = checked;
264+
this._thumbEl.classList.add('md-dragging');
265+
}
266+
}
267+
268+
/** Stops the current drag and returns the new checked value. */
269+
stopThumbDrag(): boolean {
270+
if (this._thumbBarWidth) {
271+
this._thumbBarWidth = null;
272+
this._thumbEl.classList.remove('md-dragging');
273+
274+
applyCssTransform(this._thumbEl, '');
275+
276+
return this._percentage > 50;
277+
}
278+
}
279+
280+
/** Updates the thumb containers position from the specified distance. */
281+
updateThumbPosition(distance: number) {
282+
if (this._thumbBarWidth) {
283+
this._percentage = this._getThumbPercentage(distance);
284+
applyCssTransform(this._thumbEl, `translate3d(${this._percentage}%, 0, 0)`);
285+
}
286+
}
287+
288+
/** Retrieves the percentage of thumb from the moved distance. */
289+
private _getThumbPercentage(distance: number) {
290+
let percentage = (distance / this._thumbBarWidth) * 100;
291+
292+
// When the toggle was initially checked, then we have to start the drag at the end.
293+
if (this._checked) {
294+
percentage += 100;
295+
}
296+
297+
return Math.max(0, Math.min(percentage, 100));
298+
}
299+
212300
}
213301

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