ipsos-components
Version:
Material Design components for Angular
373 lines (306 loc) • 13 kB
text/typescript
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {FocusMonitor, FocusOrigin} from '@angular/cdk/a11y';
import {coerceBooleanProperty} from '@angular/cdk/coercion';
import {Platform} from '@angular/cdk/platform';
import {
AfterContentInit,
Attribute,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
forwardRef,
Input,
OnDestroy,
Output,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
import {
applyCssTransform,
CanColor,
CanDisable,
CanDisableRipple,
HammerInput,
HasTabIndex,
MatRipple,
mixinColor,
mixinDisabled,
mixinDisableRipple,
mixinTabIndex,
RippleConfig,
RippleRef,
} from '@angular/material/core';
// Increasing integer for generating unique ids for slide-toggle components.
let nextUniqueId = 0;
export const MAT_SLIDE_TOGGLE_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MatSlideToggle),
multi: true
};
/** Change event object emitted by a MatSlideToggle. */
export class MatSlideToggleChange {
source: MatSlideToggle;
checked: boolean;
}
// Boilerplate for applying mixins to MatSlideToggle.
/** @docs-private */
export class MatSlideToggleBase {
constructor(public _elementRef: ElementRef) {}
}
export const _MatSlideToggleMixinBase =
mixinTabIndex(mixinColor(mixinDisableRipple(mixinDisabled(MatSlideToggleBase)), 'accent'));
/** Represents a slidable "switch" toggle that can be moved between on and off. */
export class MatSlideToggle extends _MatSlideToggleMixinBase implements OnDestroy, AfterContentInit,
ControlValueAccessor, CanDisable, CanColor, HasTabIndex, CanDisableRipple {
private onChange = (_: any) => {};
private onTouched = () => {};
private _uniqueId: string = `mat-slide-toggle-${++nextUniqueId}`;
private _slideRenderer: SlideToggleRenderer;
private _required: boolean = false;
private _checked: boolean = false;
/** Reference to the focus state ripple. */
private _focusRipple: RippleRef | null;
/** Name value will be applied to the input element if present */
name: string | null = null;
/** A unique id for the slide-toggle input. If none is supplied, it will be auto-generated. */
id: string = this._uniqueId;
/** Whether the label should appear after or before the slide-toggle. Defaults to 'after' */
labelPosition: 'before' | 'after' = 'after';
/** Whether the slide-toggle element is checked or not */
/** Used to set the aria-label attribute on the underlying input element. */
ariaLabel: string | null = null;
/** Used to set the aria-labelledby attribute on the underlying input element. */
ariaLabelledby: string | null = null;
/** Whether the slide-toggle is required. */
get required(): boolean { return this._required; }
set required(value) { this._required = coerceBooleanProperty(value); }
/** Whether the slide-toggle element is checked or not */
get checked(): boolean { return this._checked; }
set checked(value) {
this._checked = coerceBooleanProperty(value);
this._changeDetectorRef.markForCheck();
}
/** An event will be dispatched each time the slide-toggle changes its value. */
change: EventEmitter<MatSlideToggleChange> = new EventEmitter<MatSlideToggleChange>();
/** Returns the unique id for the visual hidden input. */
get inputId(): string { return `${this.id || this._uniqueId}-input`; }
/** Reference to the underlying input element. */
_inputElement: ElementRef;
/** Reference to the ripple directive on the thumb container. */
_ripple: MatRipple;
/** Ripple configuration for the mouse ripples and focus indicators. */
_rippleConfig: RippleConfig = {centered: true, radius: 23, speedFactor: 1.5};
constructor(elementRef: ElementRef,
private _platform: Platform,
private _focusMonitor: FocusMonitor,
private _changeDetectorRef: ChangeDetectorRef,
tabIndex: string) {
super(elementRef);
this.tabIndex = parseInt(tabIndex) || 0;
}
ngAfterContentInit() {
this._slideRenderer = new SlideToggleRenderer(this._elementRef, this._platform);
this._focusMonitor
.monitor(this._inputElement.nativeElement, false)
.subscribe(focusOrigin => this._onInputFocusChange(focusOrigin));
}
ngOnDestroy() {
this._focusMonitor.stopMonitoring(this._inputElement.nativeElement);
}
/** Method being called whenever the underlying input emits a change event. */
_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();
// Releasing the pointer over the `<label>` element while dragging triggers another
// click event on the `<label>` element. This means that the checked state of the underlying
// input changed unintentionally and needs to be changed back.
if (this._slideRenderer.dragging) {
this._inputElement.nativeElement.checked = this.checked;
return;
}
// Sync the value from the underlying input element with the component instance.
this.checked = this._inputElement.nativeElement.checked;
// Emit our custom change event only if the underlying input emitted one. This ensures that
// there is no change event, when the checked state changes programmatically.
this._emitChangeEvent();
}
/** Method being called whenever the slide-toggle has been clicked. */
_onInputClick(event: Event) {
// 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();
}
/** Implemented as part of ControlValueAccessor. */
writeValue(value: any): void {
this.checked = !!value;
}
/** Implemented as part of ControlValueAccessor. */
registerOnChange(fn: any): void {
this.onChange = fn;
}
/** Implemented as part of ControlValueAccessor. */
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
/** Implemented as a part of ControlValueAccessor. */
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
this._changeDetectorRef.markForCheck();
}
/** Focuses the slide-toggle. */
focus() {
this._focusMonitor.focusVia(this._inputElement.nativeElement, 'keyboard');
}
/** Toggles the checked state of the slide-toggle. */
toggle() {
this.checked = !this.checked;
}
/** Function is called whenever the focus changes for the input element. */
private _onInputFocusChange(focusOrigin: FocusOrigin) {
if (!this._focusRipple && focusOrigin === 'keyboard') {
// For keyboard focus show a persistent ripple as focus indicator.
this._focusRipple = this._ripple.launch(0, 0, {persistent: true, ...this._rippleConfig});
} else if (!focusOrigin) {
this.onTouched();
// Fade out and clear the focus ripple if one is currently present.
if (this._focusRipple) {
this._focusRipple.fadeOut();
this._focusRipple = null;
}
}
}
/**
* Emits a change event on the `change` output. Also notifies the FormControl about the change.
*/
private _emitChangeEvent() {
let event = new MatSlideToggleChange();
event.source = this;
event.checked = this.checked;
this.onChange(this.checked);
this.change.emit(event);
}
_onDragStart() {
if (!this.disabled) {
this._slideRenderer.startThumbDrag(this.checked);
}
}
_onDrag(event: HammerInput) {
if (this._slideRenderer.dragging) {
this._slideRenderer.updateThumbPosition(event.deltaX);
}
}
_onDragEnd() {
if (this._slideRenderer.dragging) {
const newCheckedValue = this._slideRenderer.dragPercentage > 50;
if (newCheckedValue !== this.checked) {
this.checked = newCheckedValue;
this._emitChangeEvent();
}
// The drag should be stopped outside of the current event handler, because otherwise the
// click event will be fired before and will revert the drag change.
setTimeout(() => this._slideRenderer.stopThumbDrag());
}
}
/** Method being called whenever the label text changes. */
_onLabelTextChange() {
// This method is getting called whenever the label of the slide-toggle changes.
// Since the slide-toggle uses the OnPush strategy we need to notify it about the change
// that has been recognized by the cdkObserveContent directive.
this._changeDetectorRef.markForCheck();
}
}
/**
* Renderer for the Slide Toggle component, which separates DOM modification in its own class
*/
class SlideToggleRenderer {
/** Reference to the thumb HTMLElement. */
private _thumbEl: HTMLElement;
/** Reference to the thumb bar HTMLElement. */
private _thumbBarEl: HTMLElement;
/** Width of the thumb bar of the slide-toggle. */
private _thumbBarWidth: number;
/** Previous checked state before drag started. */
private _previousChecked: boolean;
/** Percentage of the thumb while dragging. Percentage as fraction of 100. */
dragPercentage: number;
/** Whether the thumb is currently being dragged. */
dragging: boolean = false;
constructor(elementRef: ElementRef, platform: Platform) {
// We only need to interact with these elements when we're on the browser, so only grab
// the reference in that case.
if (platform.isBrowser) {
this._thumbEl = elementRef.nativeElement.querySelector('.mat-slide-toggle-thumb-container');
this._thumbBarEl = elementRef.nativeElement.querySelector('.mat-slide-toggle-bar');
}
}
/** Initializes the drag of the slide-toggle. */
startThumbDrag(checked: boolean) {
if (this.dragging) { return; }
this._thumbBarWidth = this._thumbBarEl.clientWidth - this._thumbEl.clientWidth;
this._thumbEl.classList.add('mat-dragging');
this._previousChecked = checked;
this.dragging = true;
}
/** Resets the current drag and returns the new checked value. */
stopThumbDrag(): boolean {
if (!this.dragging) { return false; }
this.dragging = false;
this._thumbEl.classList.remove('mat-dragging');
// Reset the transform because the component will take care of the thumb position after drag.
applyCssTransform(this._thumbEl, '');
return this.dragPercentage > 50;
}
/** Updates the thumb containers position from the specified distance. */
updateThumbPosition(distance: number) {
this.dragPercentage = this._getDragPercentage(distance);
// Calculate the moved distance based on the thumb bar width.
let dragX = (this.dragPercentage / 100) * this._thumbBarWidth;
applyCssTransform(this._thumbEl, `translate3d(${dragX}px, 0, 0)`);
}
/** Retrieves the percentage of thumb from the moved distance. Percentage as fraction of 100. */
private _getDragPercentage(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._previousChecked) {
percentage += 100;
}
return Math.max(0, Math.min(percentage, 100));
}
}