/** * @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} from '@angular/cdk/a11y'; import {Directionality} from '@angular/cdk/bidi'; import {coerceBooleanProperty} from '@angular/cdk/coercion'; import { AfterContentInit, Attribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, forwardRef, Input, OnDestroy, Output, ViewChild, ViewEncapsulation, NgZone, Optional, Inject, } from '@angular/core'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; import { CanColor, CanColorCtor, CanDisable, CanDisableCtor, CanDisableRipple, CanDisableRippleCtor, HammerInput, HasTabIndex, HasTabIndexCtor, mixinColor, mixinDisabled, mixinDisableRipple, mixinTabIndex, } from '@angular/material/core'; import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations'; import { MAT_SLIDE_TOGGLE_DEFAULT_OPTIONS, MatSlideToggleDefaultOptions } from './slide-toggle-config'; // Increasing integer for generating unique ids for slide-toggle components. let nextUniqueId = 0; /** @docs-private */ 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 { constructor( /** The source MatSlideToggle of the event. */ public source: MatSlideToggle, /** The new `checked` value of the MatSlideToggle. */ public checked: boolean) { } } // Boilerplate for applying mixins to MatSlideToggle. /** @docs-private */ class MatSlideToggleBase { constructor(public _elementRef: ElementRef) {} } const _MatSlideToggleMixinBase: HasTabIndexCtor & CanColorCtor & CanDisableRippleCtor & CanDisableCtor & typeof MatSlideToggleBase = mixinTabIndex(mixinColor(mixinDisableRipple(mixinDisabled(MatSlideToggleBase)), 'accent')); /** Represents a slidable "switch" toggle that can be moved between on and off. */ @Component({ moduleId: module.id, selector: 'mat-slide-toggle', exportAs: 'matSlideToggle', host: { 'class': 'mat-slide-toggle', '[id]': 'id', // Needs to be `-1` so it can still receive programmatic focus. '[attr.tabindex]': 'disabled ? null : -1', '[class.mat-checked]': 'checked', '[class.mat-disabled]': 'disabled', '[class.mat-slide-toggle-label-before]': 'labelPosition == "before"', '[class._mat-animation-noopable]': '_animationMode === "NoopAnimations"', '(focus)': '_inputElement.nativeElement.focus()', }, templateUrl: 'slide-toggle.html', styleUrls: ['slide-toggle.css'], providers: [MAT_SLIDE_TOGGLE_VALUE_ACCESSOR], inputs: ['disabled', 'disableRipple', 'color', 'tabIndex'], encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, }) 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 _required: boolean = false; private _checked: boolean = false; /** Whether the thumb is currently being dragged. */ private _dragging = false; /** Previous checked state before drag started. */ private _previousChecked: boolean; /** Width of the thumb bar of the slide-toggle. */ private _thumbBarWidth: number; /** Percentage of the thumb while dragging. Percentage as fraction of 100. */ private _dragPercentage: number; /** Reference to the thumb HTMLElement. */ @ViewChild('thumbContainer', {static: false}) _thumbEl: ElementRef; /** Reference to the thumb bar HTMLElement. */ @ViewChild('toggleBar', {static: false}) _thumbBarEl: ElementRef; /** Name value will be applied to the input element if present. */ @Input() name: string | null = null; /** A unique id for the slide-toggle input. If none is supplied, it will be auto-generated. */ @Input() id: string = this._uniqueId; /** Whether the label should appear after or before the slide-toggle. Defaults to 'after'. */ @Input() labelPosition: 'before' | 'after' = 'after'; /** Used to set the aria-label attribute on the underlying input element. */ @Input('aria-label') ariaLabel: string | null = null; /** Used to set the aria-labelledby attribute on the underlying input element. */ @Input('aria-labelledby') ariaLabelledby: string | null = null; /** Whether the slide-toggle is required. */ @Input() get required(): boolean { return this._required; } set required(value) { this._required = coerceBooleanProperty(value); } /** Whether the slide-toggle element is checked or not. */ @Input() 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. */ @Output() readonly change: EventEmitter = new EventEmitter(); /** * An event will be dispatched each time the slide-toggle input is toggled. * This event is always emitted when the user toggles the slide toggle, but this does not mean * the slide toggle's value has changed. The event does not fire when the user drags to change * the slide toggle value. */ @Output() readonly toggleChange: EventEmitter = new EventEmitter(); /** * An event will be dispatched each time the slide-toggle is dragged. * This event is always emitted when the user drags the slide toggle to make a change greater * than 50%. It does not mean the slide toggle's value is changed. The event is not emitted when * the user toggles the slide toggle to change its value. */ @Output() readonly dragChange: EventEmitter = new EventEmitter(); /** Returns the unique id for the visual hidden input. */ get inputId(): string { return `${this.id || this._uniqueId}-input`; } /** Reference to the underlying input element. */ @ViewChild('input', {static: false}) _inputElement: ElementRef; constructor(elementRef: ElementRef, private _focusMonitor: FocusMonitor, private _changeDetectorRef: ChangeDetectorRef, @Attribute('tabindex') tabIndex: string, private _ngZone: NgZone, @Inject(MAT_SLIDE_TOGGLE_DEFAULT_OPTIONS) public defaults: MatSlideToggleDefaultOptions, @Optional() @Inject(ANIMATION_MODULE_TYPE) public _animationMode?: string, @Optional() private _dir?: Directionality) { super(elementRef); this.tabIndex = parseInt(tabIndex) || 0; } ngAfterContentInit() { this._focusMonitor .monitor(this._elementRef, true) .subscribe(focusOrigin => { if (!focusOrigin) { // When a focused element becomes disabled, the browser *immediately* fires a blur event. // Angular does not expect events to be raised during change detection, so any state // change (such as a form control's 'ng-touched') will cause a changed-after-checked // error. See https://github.com/angular/angular/issues/17793. To work around this, // we defer telling the form control it has been touched until the next tick. Promise.resolve().then(() => this._onTouched()); } }); } ngOnDestroy() { this._focusMonitor.stopMonitoring(this._elementRef); } /** 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(); if (!this._dragging) { this.toggleChange.emit(); } // Releasing the pointer over the `