-
Notifications
You must be signed in to change notification settings - Fork 6.8k
/
Copy pathslide-toggle.ts
293 lines (257 loc) · 11.1 KB
/
slide-toggle.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
/**
* @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 {BooleanInput, coerceBooleanProperty, NumberInput} from '@angular/cdk/coercion';
import {
AfterContentInit,
Attribute,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
forwardRef,
Input,
OnDestroy,
Output,
ViewChild,
ViewEncapsulation,
Optional,
Inject,
} from '@angular/core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
import {
CanColor, CanColorCtor,
CanDisable, CanDisableCtor,
CanDisableRipple, CanDisableRippleCtor,
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({
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',
'[attr.aria-label]': 'null',
'[attr.aria-labelledby]': 'null',
'[class.mat-checked]': 'checked',
'[class.mat-disabled]': 'disabled',
'[class.mat-slide-toggle-label-before]': 'labelPosition == "before"',
'[class._mat-animation-noopable]': '_animationMode === "NoopAnimations"',
},
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;
/** Reference to the thumb HTMLElement. */
@ViewChild('thumbContainer') _thumbEl: ElementRef;
/** Reference to the thumb bar HTMLElement. */
@ViewChild('toggleBar') _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<MatSlideToggleChange> =
new EventEmitter<MatSlideToggleChange>();
/**
* 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.
*/
@Output() readonly toggleChange: EventEmitter<void> = new EventEmitter<void>();
/** 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') _inputElement: ElementRef<HTMLInputElement>;
constructor(elementRef: ElementRef,
private _focusMonitor: FocusMonitor,
private _changeDetectorRef: ChangeDetectorRef,
@Attribute('tabindex') tabIndex: string,
@Inject(MAT_SLIDE_TOGGLE_DEFAULT_OPTIONS)
public defaults: MatSlideToggleDefaultOptions,
@Optional() @Inject(ANIMATION_MODULE_TYPE) public _animationMode?: string) {
super(elementRef);
this.tabIndex = parseInt(tabIndex) || 0;
}
ngAfterContentInit() {
this._focusMonitor
.monitor(this._elementRef, true)
.subscribe(focusOrigin => {
// Only forward focus manually when it was received programmatically or through the
// keyboard. We should not do this for mouse/touch focus for two reasons:
// 1. It can prevent clicks from landing in Chrome (see #18269).
// 2. They're already handled by the wrapping `label` element.
if (focusOrigin === 'keyboard' || focusOrigin === 'program') {
this._inputElement.nativeElement.focus();
} else 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();
this.toggleChange.emit();
// When the slide toggle's config disables toggle change event by setting
// `disableToggleValue: true`, the slide toggle's value does not change, and the
// checked state of the underlying input needs to be changed back.
if (this.defaults.disableToggleValue) {
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(options?: FocusOptions, origin?: FocusOrigin): void {
if (origin) {
this._focusMonitor.focusVia(this._inputElement, origin, options);
} else {
this._inputElement.nativeElement.focus(options);
}
}
/** Toggles the checked state of the slide-toggle. */
toggle(): void {
this.checked = !this.checked;
this._onChange(this.checked);
}
/**
* Emits a change event on the `change` output. Also notifies the FormControl about the change.
*/
private _emitChangeEvent() {
this._onChange(this.checked);
this.change.emit(new MatSlideToggleChange(this, this.checked));
}
/** Method being called whenever the label text changes. */
_onLabelTextChange() {
// Since the event of the `cdkObserveContent` directive runs outside of the zone, the
// slide-toggle component will be only marked for check, but no actual change detection runs
// automatically. Instead of going back into the zone in order to trigger a change detection
// which causes *all* components to be checked (if explicitly marked or not using OnPush),
// we only trigger an explicit change detection for the slide-toggle view and its children.
this._changeDetectorRef.detectChanges();
}
static ngAcceptInputType_required: BooleanInput;
static ngAcceptInputType_checked: BooleanInput;
static ngAcceptInputType_disabled: BooleanInput;
static ngAcceptInputType_disableRipple: BooleanInput;
static ngAcceptInputType_tabIndex: NumberInput;
}