UNPKG

ipsos-components

Version:

Material Design components for Angular

492 lines (415 loc) 15.7 kB
/** * @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 {coerceBooleanProperty} from '@angular/cdk/coercion'; import {UniqueSelectionDispatcher} from '@angular/cdk/collections'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChildren, Directive, ElementRef, EventEmitter, forwardRef, Input, OnDestroy, OnInit, Optional, Output, QueryList, ViewChild, ViewEncapsulation, } from '@angular/core'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; import {CanDisable, mixinDisabled} from '@angular/material/core'; /** Acceptable types for a button toggle. */ export type ToggleType = 'checkbox' | 'radio'; // Boilerplate for applying mixins to MatButtonToggleGroup and MatButtonToggleGroupMultiple /** @docs-private */ export class MatButtonToggleGroupBase {} export const _MatButtonToggleGroupMixinBase = mixinDisabled(MatButtonToggleGroupBase); /** * Provider Expression that allows mat-button-toggle-group to register as a ControlValueAccessor. * This allows it to support [(ngModel)]. * @docs-private */ export const MAT_BUTTON_TOGGLE_GROUP_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => MatButtonToggleGroup), multi: true }; let _uniqueIdCounter = 0; /** Change event object emitted by MatButtonToggle. */ export class MatButtonToggleChange { /** The MatButtonToggle that emits the event. */ source: MatButtonToggle | null; /** The value assigned to the MatButtonToggle. */ value: any; } /** Exclusive selection button toggle group that behaves like a radio-button group. */ @Directive({ selector: 'mat-button-toggle-group:not([multiple])', providers: [MAT_BUTTON_TOGGLE_GROUP_VALUE_ACCESSOR], inputs: ['disabled'], host: { 'role': 'radiogroup', 'class': 'mat-button-toggle-group', '[class.mat-button-toggle-vertical]': 'vertical' }, exportAs: 'matButtonToggleGroup', }) export class MatButtonToggleGroup extends _MatButtonToggleGroupMixinBase implements ControlValueAccessor, CanDisable { /** The value for the button toggle group. Should match currently selected button toggle. */ private _value: any = null; /** The HTML name attribute applied to toggles in this group. */ private _name: string = `mat-button-toggle-group-${_uniqueIdCounter++}`; /** Whether the button toggle group should be vertical. */ private _vertical: boolean = false; /** The currently selected button toggle, should match the value. */ private _selected: MatButtonToggle | null = null; /** * The method to be called in order to update ngModel. * Now `ngModel` binding is not supported in multiple selection mode. */ _controlValueAccessorChangeFn: (value: any) => void = () => {}; /** onTouch function registered via registerOnTouch (ControlValueAccessor). */ _onTouched: () => any = () => {}; /** Child button toggle buttons. */ @ContentChildren(forwardRef(() => MatButtonToggle)) _buttonToggles: QueryList<MatButtonToggle>; /** `name` attribute for the underlying `input` element. */ @Input() get name(): string { return this._name; } set name(value: string) { this._name = value; this._updateButtonToggleNames(); } /** Whether the toggle group is vertical. */ @Input() get vertical(): boolean { return this._vertical; } set vertical(value) { this._vertical = coerceBooleanProperty(value); } /** Value of the toggle group. */ @Input() get value(): any { return this._value; } set value(newValue: any) { if (this._value != newValue) { this._value = newValue; this.valueChange.emit(newValue); this._updateSelectedButtonToggleFromValue(); } } /** * Event that emits whenever the value of the group changes. * Used to facilitate two-way data binding. * @docs-private */ @Output() valueChange = new EventEmitter<any>(); /** Whether the toggle group is selected. */ @Input() get selected() { return this._selected; } set selected(selected: MatButtonToggle | null) { this._selected = selected; this.value = selected ? selected.value : null; if (selected && !selected.checked) { selected.checked = true; } } /** Event emitted when the group's value changes. */ @Output() change: EventEmitter<MatButtonToggleChange> = new EventEmitter<MatButtonToggleChange>(); constructor(private _changeDetector: ChangeDetectorRef) { super(); } private _updateButtonToggleNames(): void { if (this._buttonToggles) { this._buttonToggles.forEach((toggle) => { toggle.name = this._name; }); } } // TODO: Refactor into shared code with radio. private _updateSelectedButtonToggleFromValue(): void { let isAlreadySelected = this._selected != null && this._selected.value == this._value; if (this._buttonToggles != null && !isAlreadySelected) { let matchingButtonToggle = this._buttonToggles.filter( buttonToggle => buttonToggle.value == this._value)[0]; if (matchingButtonToggle) { this.selected = matchingButtonToggle; } else if (this.value == null) { this.selected = null; this._buttonToggles.forEach(buttonToggle => { buttonToggle.checked = false; }); } } } /** Dispatch change event with current selection and group value. */ _emitChangeEvent(): void { let event = new MatButtonToggleChange(); event.source = this._selected; event.value = this._value; this._controlValueAccessorChangeFn(event.value); this.change.emit(event); } /** * Sets the model value. Implemented as part of ControlValueAccessor. * @param value Value to be set to the model. */ writeValue(value: any) { this.value = value; this._changeDetector.markForCheck(); } /** * Registers a callback that will be triggered when the value has changed. * Implemented as part of ControlValueAccessor. * @param fn On change callback function. */ registerOnChange(fn: (value: any) => void) { this._controlValueAccessorChangeFn = fn; } /** * Registers a callback that will be triggered when the control has been touched. * Implemented as part of ControlValueAccessor. * @param fn On touch callback function. */ registerOnTouched(fn: any) { this._onTouched = fn; } /** * Toggles the disabled state of the component. Implemented as part of ControlValueAccessor. * @param isDisabled Whether the component should be disabled. */ setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; this._markButtonTogglesForCheck(); } private _markButtonTogglesForCheck() { if (this._buttonToggles) { this._buttonToggles.forEach((toggle) => toggle._markForCheck()); } } } /** Multiple selection button-toggle group. `ngModel` is not supported in this mode. */ @Directive({ selector: 'mat-button-toggle-group[multiple]', exportAs: 'matButtonToggleGroup', inputs: ['disabled'], host: { 'class': 'mat-button-toggle-group', '[class.mat-button-toggle-vertical]': 'vertical', 'role': 'group' } }) export class MatButtonToggleGroupMultiple extends _MatButtonToggleGroupMixinBase implements CanDisable { /** Whether the button toggle group should be vertical. */ private _vertical: boolean = false; /** Whether the toggle group is vertical. */ @Input() get vertical(): boolean { return this._vertical; } set vertical(value) { this._vertical = coerceBooleanProperty(value); } } /** Single button inside of a toggle group. */ @Component({ moduleId: module.id, selector: 'mat-button-toggle', templateUrl: 'button-toggle.html', styleUrls: ['button-toggle.css'], encapsulation: ViewEncapsulation.None, preserveWhitespaces: false, exportAs: 'matButtonToggle', changeDetection: ChangeDetectionStrategy.OnPush, host: { '[class.mat-button-toggle-standalone]': '!buttonToggleGroup && !buttonToggleGroupMultiple', '[class.mat-button-toggle-checked]': 'checked', '[class.mat-button-toggle-disabled]': 'disabled', 'class': 'mat-button-toggle', '[attr.id]': 'id', } }) export class MatButtonToggle implements OnInit, OnDestroy { /** * Attached to the aria-label attribute of the host element. In most cases, arial-labelledby will * take precedence so this may be omitted. */ @Input('aria-label') ariaLabel: string = ''; /** * Users can specify the `aria-labelledby` attribute which will be forwarded to the input element */ @Input('aria-labelledby') ariaLabelledby: string | null = null; /** Whether or not this button toggle is checked. */ private _checked: boolean = false; /** Type of the button toggle. Either 'radio' or 'checkbox'. */ _type: ToggleType; /** Whether or not this button toggle is disabled. */ private _disabled: boolean = false; /** Value assigned to this button toggle. */ private _value: any = null; /** Whether or not the button toggle is a single selection. */ private _isSingleSelector: boolean = false; /** Unregister function for _buttonToggleDispatcher **/ private _removeUniqueSelectionListener: () => void = () => {}; @ViewChild('input') _inputElement: ElementRef; /** The parent button toggle group (exclusive selection). Optional. */ buttonToggleGroup: MatButtonToggleGroup; /** The parent button toggle group (multiple selection). Optional. */ buttonToggleGroupMultiple: MatButtonToggleGroupMultiple; /** Unique ID for the underlying `input` element. */ get inputId(): string { return `${this.id}-input`; } /** The unique ID for this button toggle. */ @Input() id: string; /** HTML's 'name' attribute used to group radios for unique selection. */ @Input() name: string; /** Whether the button is checked. */ @Input() get checked(): boolean { return this._checked; } set checked(newCheckedState: boolean) { if (this._isSingleSelector && newCheckedState) { // Notify all button toggles with the same name (in the same group) to un-check. this._buttonToggleDispatcher.notify(this.id, this.name); this._changeDetectorRef.markForCheck(); } this._checked = newCheckedState; if (newCheckedState && this._isSingleSelector && this.buttonToggleGroup.value != this.value) { this.buttonToggleGroup.selected = this; } } /** MatButtonToggleGroup reads this to assign its own value. */ @Input() get value(): any { return this._value; } set value(value: any) { if (this._value != value) { if (this.buttonToggleGroup != null && this.checked) { this.buttonToggleGroup.value = value; } this._value = value; } } /** Whether the button is disabled. */ @Input() get disabled(): boolean { return this._disabled || (this.buttonToggleGroup != null && this.buttonToggleGroup.disabled) || (this.buttonToggleGroupMultiple != null && this.buttonToggleGroupMultiple.disabled); } set disabled(value: boolean) { this._disabled = coerceBooleanProperty(value); } /** Event emitted when the group value changes. */ @Output() change: EventEmitter<MatButtonToggleChange> = new EventEmitter<MatButtonToggleChange>(); constructor(@Optional() toggleGroup: MatButtonToggleGroup, @Optional() toggleGroupMultiple: MatButtonToggleGroupMultiple, private _changeDetectorRef: ChangeDetectorRef, private _buttonToggleDispatcher: UniqueSelectionDispatcher, private _elementRef: ElementRef, private _focusMonitor: FocusMonitor) { this.buttonToggleGroup = toggleGroup; this.buttonToggleGroupMultiple = toggleGroupMultiple; if (this.buttonToggleGroup) { this._removeUniqueSelectionListener = _buttonToggleDispatcher.listen((id: string, name: string) => { if (id != this.id && name == this.name) { this.checked = false; this._changeDetectorRef.markForCheck(); } }); this._type = 'radio'; this.name = this.buttonToggleGroup.name; this._isSingleSelector = true; } else { // Even if there is no group at all, treat the button toggle as a checkbox so it can be // toggled on or off. this._type = 'checkbox'; this._isSingleSelector = false; } } ngOnInit() { if (this.id == null) { this.id = `mat-button-toggle-${_uniqueIdCounter++}`; } if (this.buttonToggleGroup && this._value == this.buttonToggleGroup.value) { this._checked = true; } this._focusMonitor.monitor(this._elementRef.nativeElement, true); } /** Focuses the button. */ focus() { this._inputElement.nativeElement.focus(); } /** Toggle the state of the current button toggle. */ private _toggle(): void { this.checked = !this.checked; } /** Checks the button toggle due to an interaction with the underlying native input. */ _onInputChange(event: Event) { event.stopPropagation(); if (this._isSingleSelector) { // Propagate the change one-way via the group, which will in turn mark this // button toggle as checked. let groupValueChanged = this.buttonToggleGroup.selected != this; this.checked = true; this.buttonToggleGroup.selected = this; this.buttonToggleGroup._onTouched(); if (groupValueChanged) { this.buttonToggleGroup._emitChangeEvent(); } } else { this._toggle(); } // Emit a change event when the native input does. this._emitChangeEvent(); } _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(); } /** Dispatch change event with current value. */ private _emitChangeEvent(): void { let event = new MatButtonToggleChange(); event.source = this; event.value = this._value; this.change.emit(event); } // Unregister buttonToggleDispatcherListener on destroy ngOnDestroy(): void { this._removeUniqueSelectionListener(); } /** * Marks the button toggle as needing checking for change detection. * This method is exposed because the parent button toggle group will directly * update bound properties of the radio button. */ _markForCheck() { // When group value changes, the button will not be notified. Use `markForCheck` to explicit // update button toggle's status this._changeDetectorRef.markForCheck(); } }