Skip to content

Commit

Permalink
feat(slide-toggle): add drag functionality to thumb (#750)
Browse files Browse the repository at this point in the history
  • Loading branch information
devversion authored and kara committed Jul 14, 2016
1 parent 9552ed5 commit 25b4f21
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 19 deletions.
8 changes: 7 additions & 1 deletion src/components/slide-toggle/slide-toggle.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
<label class="md-slide-toggle-label">

<div class="md-slide-toggle-container">
<div class="md-slide-toggle-bar"></div>
<div class="md-slide-toggle-thumb-container">

<div class="md-slide-toggle-thumb-container"
(slidestart)="_onDragStart($event)"
(slide)="_onDrag($event)"
(slideend)="_onDragEnd($event)">

<div class="md-slide-toggle-thumb">
<div class="md-ink-ripple"></div>
</div>
Expand Down
6 changes: 6 additions & 0 deletions src/components/slide-toggle/slide-toggle.scss
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,12 @@ $md-slide-toggle-margin: 16px !default;

transition: $swift-linear;
transition-property: transform;

// Once the thumb container is being dragged around, we remove the transition duration to
// make the drag feeling fast and not delayed.
&.md-dragging {
transition-duration: 0ms;
}
}

// The thumb will be elevated from the slide-toggle bar.
Expand Down
100 changes: 94 additions & 6 deletions src/components/slide-toggle/slide-toggle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ import {
ControlValueAccessor,
NG_VALUE_ACCESSOR
} from '@angular/forms';
import { BooleanFieldValue } from '@angular2-material/core/annotations/field-value';
import { Observable } from 'rxjs/Observable';
import {BooleanFieldValue} from '@angular2-material/core/annotations/field-value';
import {Observable} from 'rxjs/Observable';
import {applyCssTransform} from '@angular2-material/core/style/apply-transform';

export const MD_SLIDE_TOGGLE_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
Expand Down Expand Up @@ -58,6 +59,7 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
private _hasFocus: boolean = false;
private _isMousedown: boolean = false;
private _isInitialized: boolean = false;
private _slideRenderer: SlideToggleRenderer = null;

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

constructor(private _elementRef: ElementRef,
private _renderer: Renderer) {
}
constructor(private _elementRef: ElementRef, private _renderer: Renderer) {}

/** TODO: internal */
ngAfterContentInit() {
this._slideRenderer = new SlideToggleRenderer(this._elementRef);

// Mark this component as initialized in AfterContentInit because the initial checked value can
// possibly be set by NgModel or the checked attribute. This would cause the change event to
// be emitted, before the component is actually initialized.
Expand All @@ -95,7 +97,8 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
// emit its event object to the component's `change` output.
event.stopPropagation();

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

/** Emits the change event to the `change` output EventEmitter */
private _emitChangeEvent() {
let event = new MdSlideToggleChange();
event.source = this;
event.checked = this.checked;
this._change.emit(event);
}


/** TODO: internal */
_onDragStart() {
this._slideRenderer.startThumbDrag(this.checked);
}

/** TODO: internal */
_onDrag(event: HammerInput) {
this._slideRenderer.updateThumbPosition(event.deltaX);
}

/** TODO: internal */
_onDragEnd() {
// Notice that we have to stop outside of the current event handler,
// because otherwise the click event will be fired and will reset the new checked variable.
setTimeout(() => {
this.checked = this._slideRenderer.stopThumbDrag();
}, 0);
}

}

/**
* Renderer for the Slide Toggle component, which separates DOM modification in its own class
*/
class SlideToggleRenderer {

private _thumbEl: HTMLElement;
private _thumbBarEl: HTMLElement;
private _thumbBarWidth: number;
private _checked: boolean;
private _percentage: number;

constructor(private _elementRef: ElementRef) {
this._thumbEl = _elementRef.nativeElement.querySelector('.md-slide-toggle-thumb-container');
this._thumbBarEl = _elementRef.nativeElement.querySelector('.md-slide-toggle-bar');
}

/** Whether the slide-toggle is currently dragging. */
isDragging(): boolean {
return !!this._thumbBarWidth;
}

/** Initializes the drag of the slide-toggle. */
startThumbDrag(checked: boolean) {
if (!this._thumbBarWidth) {
this._thumbBarWidth = this._thumbBarEl.clientWidth - this._thumbEl.clientWidth;
this._checked = checked;
this._thumbEl.classList.add('md-dragging');
}
}

/** Stops the current drag and returns the new checked value. */
stopThumbDrag(): boolean {
if (this._thumbBarWidth) {
this._thumbBarWidth = null;
this._thumbEl.classList.remove('md-dragging');

applyCssTransform(this._thumbEl, '');

return this._percentage > 50;
}
}

/** Updates the thumb containers position from the specified distance. */
updateThumbPosition(distance: number) {
if (this._thumbBarWidth) {
this._percentage = this._getThumbPercentage(distance);
applyCssTransform(this._thumbEl, `translate3d(${this._percentage}%, 0, 0)`);
}
}

/** Retrieves the percentage of thumb from the moved distance. */
private _getThumbPercentage(distance: number) {
let percentage = (distance / this._thumbBarWidth) * 100;

// When the toggle was initially checked, then we have to start the drag at the end.
if (this._checked) {
percentage += 100;
}

return Math.max(0, Math.min(percentage, 100));
}

}

export const MD_SLIDE_TOGGLE_DIRECTIVES = [MdSlideToggle];
40 changes: 28 additions & 12 deletions src/core/gestures/MdGestureConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {HammerGestureConfig} from '@angular/platform-browser';
/* Adjusts configuration of our gesture library, Hammer. */
@Injectable()
export class MdGestureConfig extends HammerGestureConfig {

/* List of new event names to add to the gesture support list */
events: string[] = [
'drag',
Expand All @@ -12,6 +13,11 @@ export class MdGestureConfig extends HammerGestureConfig {
'dragright',
'dragleft',
'longpress',
'slide',
'slidestart',
'slideend',
'slideright',
'slideleft'
];

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

// create custom gesture recognizers
var drag = new Hammer.Pan({event: 'drag', threshold: 6});
var longpress = new Hammer.Press({event: 'longpress', time: 500});
// Create custom gesture recognizers
let drag = this._createRecognizer(Hammer.Pan, {event: 'drag', threshold: 6}, Hammer.Swipe);
let slide = this._createRecognizer(Hammer.Pan, {event: 'slide', threshold: 0}, Hammer.Swipe);
let longpress = this._createRecognizer(Hammer.Press, {event: 'longpress', time: 500});

let pan = new Hammer.Pan();
let swipe = new Hammer.Swipe();

// ensure custom recognizers can coexist with the default gestures (i.e. pan, press, swipe)
var pan = new Hammer.Pan();
var press = new Hammer.Press();
var swipe = new Hammer.Swipe();
drag.recognizeWith(pan);
drag.recognizeWith(swipe);
// Overwrite the default `pan` event to use the swipe event.
pan.recognizeWith(swipe);
longpress.recognizeWith(press);

// add customized gestures to Hammer manager
mc.add([drag, pan, swipe, press, longpress]);
// Add customized gestures to Hammer manager
mc.add([drag, slide, pan, longpress]);

return mc;
}

/** Creates a new recognizer, without affecting the default recognizers of HammerJS */
private _createRecognizer(type: RecognizerStatic, options: any, ...extra: RecognizerStatic[]) {
let recognizer = new type(options);

// Add the default recognizer to the new custom recognizer.
extra.push(type);
extra.forEach(entry => recognizer.recognizeWith(new entry()));

return recognizer;
}

}
1 change: 1 addition & 0 deletions test/karma.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export function config(config) {
],
files: [
{pattern: 'dist/vendor/core-js/client/core.js', included: true, watched: false},
{pattern: 'dist/vendor/hammerjs/hammer.min.js', included: true, watched: false},
{pattern: 'dist/vendor/systemjs/dist/system-polyfills.js', included: true, watched: false},
{pattern: 'dist/vendor/systemjs/dist/system.src.js', included: true, watched: false},
{pattern: 'dist/vendor/zone.js/dist/zone.js', included: true, watched: false},
Expand Down

0 comments on commit 25b4f21

Please sign in to comment.