mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
chore: remove BooleanFieldValue (#1290)
This commit is contained in:
@@ -33,3 +33,4 @@
|
||||
/libpeerconnection.log
|
||||
npm-debug.log
|
||||
testem.log
|
||||
/.chrome
|
||||
|
||||
@@ -15,13 +15,9 @@ import {
|
||||
forwardRef,
|
||||
AfterViewInit
|
||||
} from '@angular/core';
|
||||
import {
|
||||
NG_VALUE_ACCESSOR,
|
||||
ControlValueAccessor,
|
||||
FormsModule,
|
||||
} from '@angular/forms';
|
||||
import {NG_VALUE_ACCESSOR, ControlValueAccessor, FormsModule} from '@angular/forms';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
import {BooleanFieldValue, MdUniqueSelectionDispatcher} from '../core';
|
||||
import {MdUniqueSelectionDispatcher, coerceBooleanProperty} from '../core';
|
||||
|
||||
export type ToggleType = 'checkbox' | 'radio';
|
||||
|
||||
@@ -102,13 +98,12 @@ export class MdButtonToggleGroup implements AfterViewInit, ControlValueAccessor
|
||||
}
|
||||
|
||||
@Input()
|
||||
@BooleanFieldValue()
|
||||
get disabled(): boolean {
|
||||
return this._disabled;
|
||||
}
|
||||
|
||||
set disabled(value) {
|
||||
this._disabled = (value != null && value !== false) ? true : null;
|
||||
this._disabled = coerceBooleanProperty(value);
|
||||
}
|
||||
|
||||
@Input()
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
ModuleWithProviders,
|
||||
} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {BooleanFieldValue, MdRippleModule} from '../core';
|
||||
import {MdRippleModule, coerceBooleanProperty} from '../core';
|
||||
|
||||
// TODO(jelbourn): Make the `isMouseDown` stuff done with one global listener.
|
||||
// TODO(kara): Convert attribute selectors to classes when attr maps become available
|
||||
@@ -41,7 +41,11 @@ export class MdButton {
|
||||
_isMouseDown: boolean = false;
|
||||
|
||||
/** Whether the ripple effect on click should be disabled. */
|
||||
@Input() @BooleanFieldValue() disableRipple: boolean = false;
|
||||
private _disableRipple: boolean = false;
|
||||
|
||||
@Input()
|
||||
get disableRipple() { return this._disableRipple; }
|
||||
set disableRipple(v) { this._disableRipple = coerceBooleanProperty(v); }
|
||||
|
||||
constructor(private _elementRef: ElementRef, private _renderer: Renderer) { }
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@ import {
|
||||
ModuleWithProviders,
|
||||
} from '@angular/core';
|
||||
import {NG_VALUE_ACCESSOR, ControlValueAccessor} from '@angular/forms';
|
||||
import {BooleanFieldValue} from '../core';
|
||||
import {coerceBooleanProperty} from '../core/coersion/boolean-property';
|
||||
|
||||
|
||||
/**
|
||||
* Monotonically increasing integer used to auto-generate unique ids for checkbox components.
|
||||
@@ -93,8 +94,12 @@ export class MdCheckbox implements ControlValueAccessor {
|
||||
return `input-${this.id}`;
|
||||
}
|
||||
|
||||
private _required: boolean;
|
||||
|
||||
/** Whether the checkbox is required or not. */
|
||||
@Input() @BooleanFieldValue() required: boolean = false;
|
||||
@Input()
|
||||
get required(): boolean { return this._required; }
|
||||
set required(value) { this._required = coerceBooleanProperty(value); }
|
||||
|
||||
/** Whether or not the checkbox should come before or after the label. */
|
||||
@Input() align: 'start' | 'end' = 'start';
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import {BooleanFieldValue} from './field-value';
|
||||
|
||||
describe('BooleanFieldValue', () => {
|
||||
it('should work for null values', () => {
|
||||
let x = new BooleanFieldValueTest();
|
||||
|
||||
x.field = null;
|
||||
expect(x.field).toBe(false);
|
||||
|
||||
x.field = undefined;
|
||||
expect(x.field).toBe(false);
|
||||
});
|
||||
|
||||
it('should work for string values', () => {
|
||||
let x = new BooleanFieldValueTest();
|
||||
|
||||
(<any>x).field = 'hello';
|
||||
expect(x.field).toBe(true);
|
||||
|
||||
(<any>x).field = 'true';
|
||||
expect(x.field).toBe(true);
|
||||
|
||||
(<any>x).field = '';
|
||||
expect(x.field).toBe(true);
|
||||
|
||||
(<any>x).field = 'false';
|
||||
expect(x.field).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
class BooleanFieldValueTest {
|
||||
@BooleanFieldValue() field: boolean;
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/**
|
||||
* Annotation Factory that allows HTML style boolean attributes. For example,
|
||||
* a field declared like this:
|
||||
|
||||
* @Directive({ selector: 'component' }) class MyComponent {
|
||||
* @Input() @BooleanFieldValueFactory() myField: boolean;
|
||||
* }
|
||||
*
|
||||
* You could set it up this way:
|
||||
* <component myField>
|
||||
* or:
|
||||
* <component myField="">
|
||||
* @deprecated
|
||||
*/
|
||||
function booleanFieldValueFactory() {
|
||||
return function booleanFieldValueMetadata(target: any, key: string): void {
|
||||
const defaultValue = target[key];
|
||||
const localKey = `__md_private_symbol_${key}`;
|
||||
target[localKey] = defaultValue;
|
||||
|
||||
Object.defineProperty(target, key, {
|
||||
get() { return (<any>this)[localKey]; },
|
||||
set(value: boolean) {
|
||||
(<any>this)[localKey] = value != null && `${value}` !== 'false';
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export { booleanFieldValueFactory as BooleanFieldValue };
|
||||
@@ -0,0 +1,48 @@
|
||||
import {coerceBooleanProperty} from './boolean-property';
|
||||
|
||||
|
||||
describe('coerceBooleanProperty', () => {
|
||||
it('should coerce undefined to false', () => {
|
||||
expect(coerceBooleanProperty(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('should coerce null to false', () => {
|
||||
expect(coerceBooleanProperty(null)).toBe(false);
|
||||
});
|
||||
|
||||
it('should coerce the empty string to true', () => {
|
||||
expect(coerceBooleanProperty('')).toBe(true);
|
||||
});
|
||||
|
||||
it('should coerce zero to true', () => {
|
||||
expect(coerceBooleanProperty(0)).toBe(true);
|
||||
});
|
||||
|
||||
it('should coerce the string "false" to false', () => {
|
||||
expect(coerceBooleanProperty('false')).toBe(false);
|
||||
});
|
||||
|
||||
it('should coerce the boolean false to false', () => {
|
||||
expect(coerceBooleanProperty(false)).toBe(false);
|
||||
});
|
||||
|
||||
it('should coerce the boolean true to true', () => {
|
||||
expect(coerceBooleanProperty(true)).toBe(true);
|
||||
});
|
||||
|
||||
it('should coerce the string "true" to true', () => {
|
||||
expect(coerceBooleanProperty('true')).toBe(true);
|
||||
});
|
||||
|
||||
it('should coerce an arbitrary string to true', () => {
|
||||
expect(coerceBooleanProperty('pink')).toBe(true);
|
||||
});
|
||||
|
||||
it('should coerce an object to true', () => {
|
||||
expect(coerceBooleanProperty({})).toBe(true);
|
||||
});
|
||||
|
||||
it('should coerce an array to true', () => {
|
||||
expect(coerceBooleanProperty([])).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
/** Coerces a data-bound value (typically a string) to a boolean. */
|
||||
export function coerceBooleanProperty(value: any): boolean {
|
||||
return value != null && `${value}` !== 'false';
|
||||
}
|
||||
@@ -70,9 +70,6 @@ export {applyCssTransform} from './style/apply-transform';
|
||||
// Error
|
||||
export {MdError} from './errors/error';
|
||||
|
||||
// Annotations.
|
||||
export {BooleanFieldValue} from './annotations/field-value';
|
||||
|
||||
// Misc
|
||||
export {ComponentType} from './overlay/generic-component-type';
|
||||
|
||||
@@ -84,6 +81,9 @@ export * from './compatibility/style-compatibility';
|
||||
// Animation
|
||||
export * from './animation/animation';
|
||||
|
||||
// Coersion
|
||||
export {coerceBooleanProperty} from './coersion/boolean-property';
|
||||
|
||||
|
||||
@NgModule({
|
||||
imports: [MdLineModule, RtlModule, MdRippleModule, PortalModule, OverlayModule, A11yModule],
|
||||
|
||||
+50
-15
@@ -18,13 +18,9 @@ import {
|
||||
ModuleWithProviders,
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
NG_VALUE_ACCESSOR,
|
||||
ControlValueAccessor,
|
||||
FormsModule,
|
||||
} from '@angular/forms';
|
||||
import {NG_VALUE_ACCESSOR, ControlValueAccessor, FormsModule} from '@angular/forms';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {BooleanFieldValue, MdError} from '../core';
|
||||
import {MdError, coerceBooleanProperty} from '../core';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
|
||||
|
||||
@@ -118,9 +114,22 @@ export class MdInput implements ControlValueAccessor, AfterContentInit, OnChange
|
||||
*/
|
||||
@Input('aria-label') ariaLabel: string;
|
||||
@Input('aria-labelledby') ariaLabelledBy: string;
|
||||
@Input('aria-disabled') @BooleanFieldValue() ariaDisabled: boolean;
|
||||
@Input('aria-required') @BooleanFieldValue() ariaRequired: boolean;
|
||||
@Input('aria-invalid') @BooleanFieldValue() ariaInvalid: boolean;
|
||||
|
||||
private _ariaDisabled: boolean;
|
||||
private _ariaRequired: boolean;
|
||||
private _ariaInvalid: boolean;
|
||||
|
||||
@Input('aria-disabled')
|
||||
get ariaDisabled(): boolean { return this._ariaDisabled; }
|
||||
set ariaDisabled(value) { this._ariaDisabled = coerceBooleanProperty(value); }
|
||||
|
||||
@Input('aria-required')
|
||||
get ariaRequired(): boolean { return this._ariaRequired; }
|
||||
set ariaRequired(value) { this._ariaRequired = coerceBooleanProperty(value); }
|
||||
|
||||
@Input('aria-invalid')
|
||||
get ariaInvalid(): boolean { return this._ariaInvalid; }
|
||||
set ariaInvalid(value) { this._ariaInvalid = coerceBooleanProperty(value); }
|
||||
|
||||
/**
|
||||
* Content directives.
|
||||
@@ -141,14 +150,11 @@ export class MdInput implements ControlValueAccessor, AfterContentInit, OnChange
|
||||
*/
|
||||
@Input() align: 'start' | 'end' = 'start';
|
||||
@Input() dividerColor: 'primary' | 'accent' | 'warn' = 'primary';
|
||||
@Input() @BooleanFieldValue() floatingPlaceholder: boolean = true;
|
||||
@Input() hintLabel: string = '';
|
||||
|
||||
@Input() autocomplete: string;
|
||||
@Input() autocorrect: string;
|
||||
@Input() autocapitalize: string;
|
||||
@Input() @BooleanFieldValue() autofocus: boolean = false;
|
||||
@Input() @BooleanFieldValue() disabled: boolean = false;
|
||||
@Input() id: string = `md-input-${nextUniqueId++}`;
|
||||
@Input() list: string = null;
|
||||
@Input() max: string | number = null;
|
||||
@@ -156,14 +162,43 @@ export class MdInput implements ControlValueAccessor, AfterContentInit, OnChange
|
||||
@Input() min: string | number = null;
|
||||
@Input() minlength: number = null;
|
||||
@Input() placeholder: string = null;
|
||||
@Input() @BooleanFieldValue() readonly: boolean = false;
|
||||
@Input() @BooleanFieldValue() required: boolean = false;
|
||||
@Input() @BooleanFieldValue() spellcheck: boolean = false;
|
||||
@Input() step: number = null;
|
||||
@Input() tabindex: number = null;
|
||||
@Input() type: string = 'text';
|
||||
@Input() name: string = null;
|
||||
|
||||
private _floatingPlaceholder: boolean = false;
|
||||
private _autofocus: boolean = false;
|
||||
private _disabled: boolean = false;
|
||||
private _readonly: boolean = false;
|
||||
private _required: boolean = false;
|
||||
private _spellcheck: boolean = false;
|
||||
|
||||
@Input()
|
||||
get floatingPlaceholder(): boolean { return this._floatingPlaceholder; }
|
||||
set floatingPlaceholder(value) { this._floatingPlaceholder = coerceBooleanProperty(value); }
|
||||
|
||||
@Input()
|
||||
get autofocus(): boolean { return this._autofocus; }
|
||||
set autofocus(value) { this._autofocus = coerceBooleanProperty(value); }
|
||||
|
||||
@Input()
|
||||
get disabled(): boolean { return this._disabled; }
|
||||
set disabled(value) { this._disabled = coerceBooleanProperty(value); }
|
||||
|
||||
@Input()
|
||||
get readonly(): boolean { return this._readonly; }
|
||||
set readonly(value) { this._readonly = coerceBooleanProperty(value); }
|
||||
|
||||
@Input()
|
||||
get required(): boolean { return this._required; }
|
||||
set required(value) { this._required = coerceBooleanProperty(value); }
|
||||
|
||||
@Input()
|
||||
get spellcheck(): boolean { return this._spellcheck; }
|
||||
set spellcheck(value) { this._spellcheck = coerceBooleanProperty(value); }
|
||||
|
||||
|
||||
private _blurEmitter: EventEmitter<FocusEvent> = new EventEmitter<FocusEvent>();
|
||||
private _focusEmitter: EventEmitter<FocusEvent> = new EventEmitter<FocusEvent>();
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@ import {
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {Dir, MdError} from '../core';
|
||||
import {Dir, MdError, coerceBooleanProperty} from '../core';
|
||||
|
||||
|
||||
/** Exception thrown when two MdSidenav are matching the same side. */
|
||||
export class MdDuplicatedSidenavError extends MdError {
|
||||
@@ -79,9 +80,7 @@ export class MdSidenav {
|
||||
@Input()
|
||||
get opened(): boolean { return this._opened; }
|
||||
set opened(v: boolean) {
|
||||
// TODO(jelbourn): this coercion goes away when BooleanFieldValue is removed.
|
||||
let booleanValue = v != null && `${v}` !== 'false';
|
||||
this.toggle(booleanValue);
|
||||
this.toggle(coerceBooleanProperty(v));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,326 +1,329 @@
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
Renderer,
|
||||
forwardRef,
|
||||
ChangeDetectionStrategy,
|
||||
Input,
|
||||
Output,
|
||||
EventEmitter,
|
||||
AfterContentInit,
|
||||
NgModule,
|
||||
ModuleWithProviders,
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core';
|
||||
import {HAMMER_GESTURE_CONFIG} from '@angular/platform-browser';
|
||||
import {
|
||||
FormsModule,
|
||||
ControlValueAccessor,
|
||||
NG_VALUE_ACCESSOR
|
||||
} from '@angular/forms';
|
||||
import {BooleanFieldValue, applyCssTransform} from '../core';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
import {MdGestureConfig} from '../core';
|
||||
|
||||
|
||||
export const MD_SLIDE_TOGGLE_VALUE_ACCESSOR: any = {
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => MdSlideToggle),
|
||||
multi: true
|
||||
};
|
||||
|
||||
// A simple change event emitted by the MdSlideToggle component.
|
||||
export class MdSlideToggleChange {
|
||||
source: MdSlideToggle;
|
||||
checked: boolean;
|
||||
}
|
||||
|
||||
// Increasing integer for generating unique ids for slide-toggle components.
|
||||
let nextId = 0;
|
||||
|
||||
@Component({
|
||||
moduleId: module.id,
|
||||
selector: 'md-slide-toggle',
|
||||
host: {
|
||||
'[class.md-checked]': 'checked',
|
||||
'[class.md-disabled]': 'disabled',
|
||||
// This md-slide-toggle prefix will change, once the temporary ripple is removed.
|
||||
'[class.md-slide-toggle-focused]': '_hasFocus',
|
||||
'(mousedown)': '_setMousedown()'
|
||||
},
|
||||
templateUrl: 'slide-toggle.html',
|
||||
styleUrls: ['slide-toggle.css'],
|
||||
providers: [MD_SLIDE_TOGGLE_VALUE_ACCESSOR],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
|
||||
|
||||
private onChange = (_: any) => {};
|
||||
private onTouched = () => {};
|
||||
|
||||
// A unique id for the slide-toggle. By default the id is auto-generated.
|
||||
private _uniqueId = `md-slide-toggle-${++nextId}`;
|
||||
private _checked: boolean = false;
|
||||
private _color: string;
|
||||
private _isMousedown: boolean = false;
|
||||
private _slideRenderer: SlideToggleRenderer = null;
|
||||
|
||||
// Needs to be public to support AOT compilation (as host binding).
|
||||
_hasFocus: boolean = false;
|
||||
|
||||
@Input() @BooleanFieldValue() disabled: boolean = false;
|
||||
@Input() @BooleanFieldValue() required: boolean = false;
|
||||
@Input() name: string = null;
|
||||
@Input() id: string = this._uniqueId;
|
||||
@Input() tabIndex: number = 0;
|
||||
@Input() ariaLabel: string = null;
|
||||
@Input() ariaLabelledby: string = null;
|
||||
|
||||
private _change: EventEmitter<MdSlideToggleChange> = new EventEmitter<MdSlideToggleChange>();
|
||||
@Output() change: Observable<MdSlideToggleChange> = this._change.asObservable();
|
||||
|
||||
// Returns the unique id for the visual hidden input.
|
||||
getInputId = () => `${this.id || this._uniqueId}-input`;
|
||||
|
||||
constructor(private _elementRef: ElementRef, private _renderer: Renderer) {}
|
||||
|
||||
/** TODO: internal */
|
||||
ngAfterContentInit() {
|
||||
this._slideRenderer = new SlideToggleRenderer(this._elementRef);
|
||||
}
|
||||
|
||||
/**
|
||||
* The onChangeEvent method will be also called on click.
|
||||
* This is because everything for the slide-toggle is wrapped inside of a label,
|
||||
* which triggers a onChange event on click.
|
||||
*/
|
||||
_onChangeEvent(event: Event) {
|
||||
// We always have to stop propagation on the change event.
|
||||
// Otherwise the change event, from the input element, will bubble up and
|
||||
// emit its event object to the component's `change` output.
|
||||
event.stopPropagation();
|
||||
|
||||
// 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();
|
||||
|
||||
// Emit our custom change event if the native input emitted one.
|
||||
// It is important to only emit it, if the native input triggered one, because
|
||||
// we don't want to trigger a change event, when the `checked` variable changes for example.
|
||||
this._emitChangeEvent();
|
||||
}
|
||||
}
|
||||
|
||||
_onInputClick(event: Event) {
|
||||
this.onTouched();
|
||||
|
||||
// We have to stop propagation for click events on the visual hidden input element.
|
||||
// By default, when a user clicks on a label element, a generated click event will be
|
||||
// dispatched on the associated input element. Since we are using a label element as our
|
||||
// root container, the click event on the `slide-toggle` will be executed twice.
|
||||
// The real click event will bubble up, and the generated click event also tries to bubble up.
|
||||
// This will lead to multiple click events.
|
||||
// Preventing bubbling for the second event will solve that issue.
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
_setMousedown() {
|
||||
// We only *show* the focus style when focus has come to the button via the keyboard.
|
||||
// The Material Design spec is silent on this topic, and without doing this, the
|
||||
// button continues to look :active after clicking.
|
||||
// @see http://marcysutton.com/button-focus-hell/
|
||||
this._isMousedown = true;
|
||||
setTimeout(() => this._isMousedown = false, 100);
|
||||
}
|
||||
|
||||
_onInputFocus() {
|
||||
// Only show the focus / ripple indicator when the focus was not triggered by a mouse
|
||||
// interaction on the component.
|
||||
if (!this._isMousedown) {
|
||||
this._hasFocus = true;
|
||||
}
|
||||
}
|
||||
|
||||
_onInputBlur() {
|
||||
this._hasFocus = false;
|
||||
this.onTouched();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implemented as part of ControlValueAccessor.
|
||||
* TODO: internal
|
||||
*/
|
||||
writeValue(value: any): void {
|
||||
this.checked = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implemented as part of ControlValueAccessor.
|
||||
* TODO: internal
|
||||
*/
|
||||
registerOnChange(fn: any): void {
|
||||
this.onChange = fn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implemented as part of ControlValueAccessor.
|
||||
* TODO: internal
|
||||
*/
|
||||
registerOnTouched(fn: any): void {
|
||||
this.onTouched = fn;
|
||||
}
|
||||
|
||||
@Input()
|
||||
get checked() {
|
||||
return !!this._checked;
|
||||
}
|
||||
|
||||
set checked(value) {
|
||||
if (this.checked !== !!value) {
|
||||
this._checked = value;
|
||||
this.onChange(this._checked);
|
||||
}
|
||||
}
|
||||
|
||||
@Input()
|
||||
get color(): string {
|
||||
return this._color;
|
||||
}
|
||||
|
||||
set color(value: string) {
|
||||
this._updateColor(value);
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.checked = !this.checked;
|
||||
}
|
||||
|
||||
private _updateColor(newColor: string) {
|
||||
this._setElementColor(this._color, false);
|
||||
this._setElementColor(newColor, true);
|
||||
this._color = newColor;
|
||||
}
|
||||
|
||||
private _setElementColor(color: string, isAdd: boolean) {
|
||||
if (color != null && color != '') {
|
||||
this._renderer.setElementClass(this._elementRef.nativeElement, `md-${color}`, isAdd);
|
||||
}
|
||||
}
|
||||
|
||||
/** 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() {
|
||||
if (!this.disabled) {
|
||||
this._slideRenderer.startThumbDrag(this.checked);
|
||||
}
|
||||
}
|
||||
|
||||
/** TODO: internal */
|
||||
_onDrag(event: HammerInput) {
|
||||
if (this._slideRenderer.isDragging()) {
|
||||
this._slideRenderer.updateThumbPosition(event.deltaX);
|
||||
}
|
||||
}
|
||||
|
||||
/** TODO: internal */
|
||||
_onDragEnd() {
|
||||
if (!this._slideRenderer.isDragging()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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();
|
||||
this._emitChangeEvent();
|
||||
}, 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.isDragging()) {
|
||||
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.isDragging()) {
|
||||
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) {
|
||||
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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@NgModule({
|
||||
imports: [FormsModule],
|
||||
exports: [MdSlideToggle],
|
||||
declarations: [MdSlideToggle],
|
||||
})
|
||||
export class MdSlideToggleModule {
|
||||
static forRoot(): ModuleWithProviders {
|
||||
return {
|
||||
ngModule: MdSlideToggleModule,
|
||||
providers: [{provide: HAMMER_GESTURE_CONFIG, useClass: MdGestureConfig}]
|
||||
};
|
||||
}
|
||||
}
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
Renderer,
|
||||
forwardRef,
|
||||
ChangeDetectionStrategy,
|
||||
Input,
|
||||
Output,
|
||||
EventEmitter,
|
||||
AfterContentInit,
|
||||
NgModule,
|
||||
ModuleWithProviders,
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core';
|
||||
import {HAMMER_GESTURE_CONFIG} from '@angular/platform-browser';
|
||||
import {FormsModule, ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
|
||||
import {applyCssTransform, coerceBooleanProperty, MdGestureConfig} from '../core';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
|
||||
|
||||
export const MD_SLIDE_TOGGLE_VALUE_ACCESSOR: any = {
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => MdSlideToggle),
|
||||
multi: true
|
||||
};
|
||||
|
||||
// A simple change event emitted by the MdSlideToggle component.
|
||||
export class MdSlideToggleChange {
|
||||
source: MdSlideToggle;
|
||||
checked: boolean;
|
||||
}
|
||||
|
||||
// Increasing integer for generating unique ids for slide-toggle components.
|
||||
let nextId = 0;
|
||||
|
||||
@Component({
|
||||
moduleId: module.id,
|
||||
selector: 'md-slide-toggle',
|
||||
host: {
|
||||
'[class.md-checked]': 'checked',
|
||||
'[class.md-disabled]': 'disabled',
|
||||
// This md-slide-toggle prefix will change, once the temporary ripple is removed.
|
||||
'[class.md-slide-toggle-focused]': '_hasFocus',
|
||||
'(mousedown)': '_setMousedown()'
|
||||
},
|
||||
templateUrl: 'slide-toggle.html',
|
||||
styleUrls: ['slide-toggle.css'],
|
||||
providers: [MD_SLIDE_TOGGLE_VALUE_ACCESSOR],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
|
||||
|
||||
private onChange = (_: any) => {};
|
||||
private onTouched = () => {};
|
||||
|
||||
// A unique id for the slide-toggle. By default the id is auto-generated.
|
||||
private _uniqueId = `md-slide-toggle-${++nextId}`;
|
||||
private _checked: boolean = false;
|
||||
private _color: string;
|
||||
private _isMousedown: boolean = false;
|
||||
private _slideRenderer: SlideToggleRenderer = null;
|
||||
private _disabled: boolean = false;
|
||||
private _required: boolean = false;
|
||||
|
||||
// Needs to be public to support AOT compilation (as host binding).
|
||||
_hasFocus: boolean = false;
|
||||
|
||||
@Input() name: string = null;
|
||||
@Input() id: string = this._uniqueId;
|
||||
@Input() tabIndex: number = 0;
|
||||
@Input() ariaLabel: string = null;
|
||||
@Input() ariaLabelledby: string = null;
|
||||
|
||||
@Input()
|
||||
get disabled(): boolean { return this._disabled; }
|
||||
set disabled(value) { this._disabled = coerceBooleanProperty(value); }
|
||||
|
||||
@Input()
|
||||
get required(): boolean { return this._required; }
|
||||
set required(value) { this._required = coerceBooleanProperty(value); }
|
||||
|
||||
private _change: EventEmitter<MdSlideToggleChange> = new EventEmitter<MdSlideToggleChange>();
|
||||
@Output() change: Observable<MdSlideToggleChange> = this._change.asObservable();
|
||||
|
||||
// Returns the unique id for the visual hidden input.
|
||||
getInputId = () => `${this.id || this._uniqueId}-input`;
|
||||
|
||||
constructor(private _elementRef: ElementRef, private _renderer: Renderer) {}
|
||||
|
||||
/** TODO: internal */
|
||||
ngAfterContentInit() {
|
||||
this._slideRenderer = new SlideToggleRenderer(this._elementRef);
|
||||
}
|
||||
|
||||
/**
|
||||
* The onChangeEvent method will be also called on click.
|
||||
* This is because everything for the slide-toggle is wrapped inside of a label,
|
||||
* which triggers a onChange event on click.
|
||||
*/
|
||||
_onChangeEvent(event: Event) {
|
||||
// We always have to stop propagation on the change event.
|
||||
// Otherwise the change event, from the input element, will bubble up and
|
||||
// emit its event object to the component's `change` output.
|
||||
event.stopPropagation();
|
||||
|
||||
// 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();
|
||||
|
||||
// Emit our custom change event if the native input emitted one.
|
||||
// It is important to only emit it, if the native input triggered one, because
|
||||
// we don't want to trigger a change event, when the `checked` variable changes for example.
|
||||
this._emitChangeEvent();
|
||||
}
|
||||
}
|
||||
|
||||
_onInputClick(event: Event) {
|
||||
this.onTouched();
|
||||
|
||||
// We have to stop propagation for click events on the visual hidden input element.
|
||||
// By default, when a user clicks on a label element, a generated click event will be
|
||||
// dispatched on the associated input element. Since we are using a label element as our
|
||||
// root container, the click event on the `slide-toggle` will be executed twice.
|
||||
// The real click event will bubble up, and the generated click event also tries to bubble up.
|
||||
// This will lead to multiple click events.
|
||||
// Preventing bubbling for the second event will solve that issue.
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
_setMousedown() {
|
||||
// We only *show* the focus style when focus has come to the button via the keyboard.
|
||||
// The Material Design spec is silent on this topic, and without doing this, the
|
||||
// button continues to look :active after clicking.
|
||||
// @see http://marcysutton.com/button-focus-hell/
|
||||
this._isMousedown = true;
|
||||
setTimeout(() => this._isMousedown = false, 100);
|
||||
}
|
||||
|
||||
_onInputFocus() {
|
||||
// Only show the focus / ripple indicator when the focus was not triggered by a mouse
|
||||
// interaction on the component.
|
||||
if (!this._isMousedown) {
|
||||
this._hasFocus = true;
|
||||
}
|
||||
}
|
||||
|
||||
_onInputBlur() {
|
||||
this._hasFocus = false;
|
||||
this.onTouched();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implemented as part of ControlValueAccessor.
|
||||
* TODO: internal
|
||||
*/
|
||||
writeValue(value: any): void {
|
||||
this.checked = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implemented as part of ControlValueAccessor.
|
||||
* TODO: internal
|
||||
*/
|
||||
registerOnChange(fn: any): void {
|
||||
this.onChange = fn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implemented as part of ControlValueAccessor.
|
||||
* TODO: internal
|
||||
*/
|
||||
registerOnTouched(fn: any): void {
|
||||
this.onTouched = fn;
|
||||
}
|
||||
|
||||
@Input()
|
||||
get checked() {
|
||||
return !!this._checked;
|
||||
}
|
||||
|
||||
set checked(value) {
|
||||
if (this.checked !== !!value) {
|
||||
this._checked = value;
|
||||
this.onChange(this._checked);
|
||||
}
|
||||
}
|
||||
|
||||
@Input()
|
||||
get color(): string {
|
||||
return this._color;
|
||||
}
|
||||
|
||||
set color(value: string) {
|
||||
this._updateColor(value);
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.checked = !this.checked;
|
||||
}
|
||||
|
||||
private _updateColor(newColor: string) {
|
||||
this._setElementColor(this._color, false);
|
||||
this._setElementColor(newColor, true);
|
||||
this._color = newColor;
|
||||
}
|
||||
|
||||
private _setElementColor(color: string, isAdd: boolean) {
|
||||
if (color != null && color != '') {
|
||||
this._renderer.setElementClass(this._elementRef.nativeElement, `md-${color}`, isAdd);
|
||||
}
|
||||
}
|
||||
|
||||
/** 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() {
|
||||
if (!this.disabled) {
|
||||
this._slideRenderer.startThumbDrag(this.checked);
|
||||
}
|
||||
}
|
||||
|
||||
/** TODO: internal */
|
||||
_onDrag(event: HammerInput) {
|
||||
if (this._slideRenderer.isDragging()) {
|
||||
this._slideRenderer.updateThumbPosition(event.deltaX);
|
||||
}
|
||||
}
|
||||
|
||||
/** TODO: internal */
|
||||
_onDragEnd() {
|
||||
if (!this._slideRenderer.isDragging()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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();
|
||||
this._emitChangeEvent();
|
||||
}, 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.isDragging()) {
|
||||
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.isDragging()) {
|
||||
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) {
|
||||
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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@NgModule({
|
||||
imports: [FormsModule],
|
||||
exports: [MdSlideToggle],
|
||||
declarations: [MdSlideToggle],
|
||||
})
|
||||
export class MdSlideToggleModule {
|
||||
static forRoot(): ModuleWithProviders {
|
||||
return {
|
||||
ngModule: MdSlideToggleModule,
|
||||
providers: [{provide: HAMMER_GESTURE_CONFIG, useClass: MdGestureConfig}]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+10
-10
@@ -9,13 +9,9 @@ import {
|
||||
AfterContentInit,
|
||||
forwardRef,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
NG_VALUE_ACCESSOR,
|
||||
ControlValueAccessor,
|
||||
FormsModule,
|
||||
} from '@angular/forms';
|
||||
import {NG_VALUE_ACCESSOR, ControlValueAccessor, FormsModule} from '@angular/forms';
|
||||
import {HAMMER_GESTURE_CONFIG} from '@angular/platform-browser';
|
||||
import {BooleanFieldValue, MdGestureConfig, applyCssTransform} from '../core';
|
||||
import {MdGestureConfig, applyCssTransform, coerceBooleanProperty} from '../core';
|
||||
import {Input as HammerInput} from 'hammerjs';
|
||||
|
||||
/**
|
||||
@@ -58,16 +54,20 @@ export class MdSlider implements AfterContentInit, ControlValueAccessor {
|
||||
/** The dimensions of the slider. */
|
||||
private _sliderDimensions: ClientRect = null;
|
||||
|
||||
private _disabled: boolean = false;
|
||||
|
||||
@Input()
|
||||
@BooleanFieldValue()
|
||||
@HostBinding('class.md-slider-disabled')
|
||||
@HostBinding('attr.aria-disabled')
|
||||
disabled: boolean = false;
|
||||
get disabled(): boolean { return this._disabled; }
|
||||
set disabled(value) { this._disabled = coerceBooleanProperty(value); }
|
||||
|
||||
/** Whether or not to show the thumb label. */
|
||||
private _thumbLabel: boolean = false;
|
||||
|
||||
@Input('thumb-label')
|
||||
@BooleanFieldValue()
|
||||
thumbLabel: boolean = false;
|
||||
get thumbLabel(): boolean { return this._thumbLabel; }
|
||||
set thumbLabel(value) { this._thumbLabel = coerceBooleanProperty(value); }
|
||||
|
||||
/** The miniumum value that the slider can have. */
|
||||
private _min: number = 0;
|
||||
|
||||
@@ -13,14 +13,14 @@ import {
|
||||
ContentChildren
|
||||
} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {PortalModule} from '../core';
|
||||
import {PortalModule, RIGHT_ARROW, LEFT_ARROW, ENTER, coerceBooleanProperty} from '../core';
|
||||
import {MdTabLabel} from './tab-label';
|
||||
import {MdTabContent} from './tab-content';
|
||||
import {MdTabLabelWrapper} from './tab-label-wrapper';
|
||||
import {MdInkBar} from './ink-bar';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
import 'rxjs/add/operator/map';
|
||||
import {RIGHT_ARROW, LEFT_ARROW, ENTER} from '../core';
|
||||
|
||||
|
||||
/** Used to generate unique ID's for each tab component */
|
||||
let nextId = 0;
|
||||
@@ -38,11 +38,10 @@ export class MdTab {
|
||||
@ContentChild(MdTabLabel) label: MdTabLabel;
|
||||
@ContentChild(MdTabContent) content: MdTabContent;
|
||||
|
||||
// TODO: Replace this when BooleanFieldValue is removed.
|
||||
private _disabled = false;
|
||||
@Input('disabled')
|
||||
set disabled(value: boolean) {
|
||||
this._disabled = (value != null && `${value}` !== 'false');
|
||||
this._disabled = coerceBooleanProperty(value);
|
||||
}
|
||||
get disabled(): boolean {
|
||||
return this._disabled;
|
||||
|
||||
Reference in New Issue
Block a user