Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(slide-toggle): add drag functionality to thumb #750

Merged
merged 3 commits into from
Jul 14, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could just be 0 without the ms (hopefully the stylelint doesn't want this?)

Copy link
Member Author

@devversion devversion Jul 9, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like it's not allowed to have just zero here, because it needs a time unit.

}
}

// 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