Repository URL to install this package:
|
Version:
1.0.0-next.10 ▾
|
/**
* @license
* FOURBURNER CONFIDENTIAL
* Unpublished Copyright (C) 2021 FourBurner Technologies, Inc. All Rights Reserved.
*
* NOTICE: All information contained herein is, and remains the property of FOURBURNER TECHNOLOGIES,
* INC. The intellectual and technical concepts contained herein are proprietary to FOURBURNER
* TECHNOLOGIES, INC. and may be covered by U.S. and Foreign Patents, patents in process, and are
* protected by trade secret or copyright law. Dissemination of this information or reproduction of
* this material is strictly forbidden unless prior written permission is obtained from FOURBURNER
* TECHNOLOGIES, INC. Access to the source code contained herein is hereby forbidden to anyone
* except current FOURBURNER TECHNOLOGIES, INC. employees, managers or contractors who have executed
* Confidentiality and Non-disclosure agreements explicitly covering such access.
*
* The copyright notice above does not evidence any actual or intended publication or disclosure of
* this source code, which includes information that is confidential and/or proprietary, and is a
* trade secret, of FOURBURNER TECHNOLOGIES, INC. ANY REPRODUCTION, MODIFICATION, DISTRIBUTION,
* PUBLIC PERFORMANCE, OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT THE EXPRESS
* WRITTEN CONSENT OF FOURBURNER TECHNOLOGIES, INC. IS STRICTLY PROHIBITED, AND IN VIOLATION OF
* APPLICABLE LAWS AND INTERNATIONAL TREATIES. THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR
* RELATED INFORMATION DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS
* CONTENTS, OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART.
*/
import {FocusMonitor} from '@angular/cdk/a11y';
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
import {SelectionModel} from '@angular/cdk/collections';
import {
AfterContentInit,
Attribute,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ContentChildren,
Directive,
ElementRef,
EventEmitter,
forwardRef,
Input,
OnDestroy,
OnInit,
Optional,
Output,
QueryList,
ViewChild,
ViewEncapsulation,
InjectionToken,
Inject,
AfterViewInit,
} from '@angular/core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
import {CanDisableRipple, mixinDisableRipple, ngDevMode} from '@twnd/ux/core';
/**
* @deprecated No longer used.
* @breaking-change 11.0.0
*/
export type ToggleType = 'checkbox'|'radio';
/** Possible appearance styles for the button toggle. */
export type TWNDButtonToggleAppearance = 'legacy'|'standard';
/**
* Represents the default options for the button toggle that can be configured
* using the `TWND_BUTTON_TOGGLE_DEFAULT_OPTIONS` injection token.
*/
export interface TWNDButtonToggleDefaultOptions {
/**
* Default appearance to be used by button toggles. Can be overridden by explicitly
* setting an appearance on a button toggle or group.
*/
appearance?: TWNDButtonToggleAppearance;
}
/**
* Injection token that can be used to configure the
* default options for all button toggles within an app.
*/
export const TWND_BUTTON_TOGGLE_DEFAULT_OPTIONS =
new InjectionToken<TWNDButtonToggleDefaultOptions>(
'TWND_BUTTON_TOGGLE_DEFAULT_OPTIONS',
);
/**
* Injection token that can be used to reference instances of `TWNDButtonToggleGroup`.
* It serves as alternative token to the actual `TWNDButtonToggleGroup` class which
* could cause unnecessary retention of the class and its component metadata.
*/
export const TWND_BUTTON_TOGGLE_GROUP = new InjectionToken<TWNDButtonToggleGroup>(
'TWNDButtonToggleGroup',
);
/**
* Provider Expression that allows twnd-button-toggle-group to register as a ControlValueAccessor.
* This allows it to support [(ngModel)].
* @docs-private
*/
export const TWND_BUTTON_TOGGLE_GROUP_VALUE_ACCESSOR: any = {
provide : NG_VALUE_ACCESSOR,
useExisting : forwardRef(() => TWNDButtonToggleGroup),
multi : true,
};
// Counter used to generate unique IDs.
let uniqueIdCounter = 0;
/** Change event object emitted by TWNDButtonToggle. */
export class TWNDButtonToggleChange
{
constructor(
/** The TWNDButtonToggle that emits the event. */
public source: TWNDButtonToggle,
/** The value assigned to the TWNDButtonToggle. */
public value: any,
)
{}
}
/** Exclusive selection button toggle group that behaves like a radio-button group. */
@Directive({
selector : 'twnd-button-toggle-group',
providers : [
TWND_BUTTON_TOGGLE_GROUP_VALUE_ACCESSOR,
{provide : TWND_BUTTON_TOGGLE_GROUP, useExisting : TWNDButtonToggleGroup},
],
host : {
'role' : 'group',
'class' : 'twnd-button-toggle-group',
'[attr.aria-disabled]' : 'disabled',
'[class.twnd-button-toggle-vertical]' : 'vertical',
'[class.twnd-button-toggle-group-appearance-standard]' : 'appearance === "standard"',
},
exportAs : 'twndButtonToggleGroup',
})
export class TWNDButtonToggleGroup implements ControlValueAccessor, OnInit, AfterContentInit
{
private _vertical = false;
private _multiple = false;
private _disabled = false;
private _selectionModel: SelectionModel<TWNDButtonToggle>;
/**
* Reference to the raw value that the consumer tried to assign. The real
* value will exclude any values from this one that don't correspond to a
* toggle. Useful for the cases where the value is assigned before the toggles
* have been initialized or at the same that they're being swapped out.
*/
private _rawValue: any;
/**
* 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(() => TWNDButtonToggle), {
// Note that this would technically pick up toggles
// from nested groups, but that's not a case that we support.
descendants : true,
})
_buttonToggles: QueryList<TWNDButtonToggle>;
/** The appearance for all the buttons in the group. */
@Input() appearance: TWNDButtonToggleAppearance;
/** `name` attribute for the underlying `input` element. */
@Input() get name(): string
{
return this._name;
}
set name(value: string)
{
this._name = value;
if (this._buttonToggles) {
this._buttonToggles.forEach(toggle => {
toggle.name = this._name;
toggle._markForCheck();
});
}
}
private _name = `twnd-button-toggle-group-${uniqueIdCounter++}`;
/** Whether the toggle group is vertical. */
@Input() get vertical(): boolean
{
return this._vertical;
}
set vertical(value: boolean)
{
this._vertical = coerceBooleanProperty(value);
}
/** Value of the toggle group. */
@Input() get value(): any
{
const selected = this._selectionModel ? this._selectionModel.selected : [];
if (this.multiple) {
return selected.map(toggle => toggle.value);
}
return selected[0] ? selected[0].value : undefined;
}
set value(newValue: any)
{
this._setSelectionByValue(newValue);
this.valueChange.emit(this.value);
}
/**
* Event that emits whenever the value of the group changes.
* Used to facilitate two-way data binding.
* @docs-private
*/
@Output() readonly valueChange = new EventEmitter<any>();
/** Selected button toggles in the group. */
get selected(): TWNDButtonToggle|TWNDButtonToggle[]
{
const selected = this._selectionModel ? this._selectionModel.selected : [];
return this.multiple ? selected : selected[0] || null;
}
/** Whether multiple button toggles can be selected. */
@Input() get multiple(): boolean
{
return this._multiple;
}
set multiple(value: boolean)
{
this._multiple = coerceBooleanProperty(value);
}
/** Whether multiple button toggle group is disabled. */
@Input() get disabled(): boolean
{
return this._disabled;
}
set disabled(value: boolean)
{
this._disabled = coerceBooleanProperty(value);
if (this._buttonToggles) {
this._buttonToggles.forEach(toggle => toggle._markForCheck());
}
}
/** Event emitted when the group's value changes. */
@Output()
readonly change:
EventEmitter<TWNDButtonToggleChange> = new EventEmitter<TWNDButtonToggleChange>();
constructor(
private _changeDetector: ChangeDetectorRef,
@Optional() @Inject(TWND_BUTTON_TOGGLE_DEFAULT_OPTIONS) defaultOptions?:
TWNDButtonToggleDefaultOptions,
)
{
this.appearance = defaultOptions && defaultOptions.appearance ? defaultOptions.appearance :
'standard';
}
ngOnInit()
{
this._selectionModel = new SelectionModel<TWNDButtonToggle>(this.multiple, undefined, false);
}
ngAfterContentInit()
{
this._selectionModel.select(...this._buttonToggles.filter(toggle => toggle.checked));
}
/**
* 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();
}
// Implemented as part of ControlValueAccessor.
registerOnChange(fn: (value: any) => void)
{
this._controlValueAccessorChangeFn = fn;
}
// Implemented as part of ControlValueAccessor.
registerOnTouched(fn: any)
{
this._onTouched = fn;
}
// Implemented as part of ControlValueAccessor.
setDisabledState(isDisabled: boolean): void
{
this.disabled = isDisabled;
}
/** Dispatch change event with current selection and group value. */
_emitChangeEvent(): void
{
const selected = this.selected;
const source = Array.isArray(selected) ? selected[selected.length - 1] : selected;
const event = new TWNDButtonToggleChange(source!, this.value);
this._controlValueAccessorChangeFn(event.value);
this.change.emit(event);
}
/**
* Syncs a button toggle's selected state with the model value.
* @param toggle Toggle to be synced.
* @param select Whether the toggle should be selected.
* @param isUserInput Whether the change was a result of a user interaction.
* @param deferEvents Whether to defer emitting the change events.
*/
_syncButtonToggle(
toggle: TWNDButtonToggle,
select: boolean,
isUserInput = false,
deferEvents = false,
)
{
// Deselect the currently-selected toggle, if we're in single-selection
// mode and the button being toggled isn't selected at the moment.
if (!this.multiple && this.selected && !toggle.checked) {
(this.selected as TWNDButtonToggle).checked = false;
}
if (this._selectionModel) {
if (select) {
this._selectionModel.select(toggle);
} else {
this._selectionModel.deselect(toggle);
}
} else {
deferEvents = true;
}
// We need to defer in some cases in order to avoid "changed after checked errors", however
// the side-effect is that we may end up updating the model value out of sequence in others
// The `deferEvents` flag allows us to decide whether to do it on a case-by-case basis.
if (deferEvents) {
Promise.resolve().then(() => this._updateModelValue(isUserInput));
} else {
this._updateModelValue(isUserInput);
}
}
/** Checks whether a button toggle is selected. */
_isSelected(toggle: TWNDButtonToggle)
{
return this._selectionModel && this._selectionModel.isSelected(toggle);
}
/** Determines whether a button toggle should be checked on init. */
_isPrechecked(toggle: TWNDButtonToggle)
{
if (typeof this._rawValue === 'undefined') {
return false;
}
if (this.multiple && Array.isArray(this._rawValue)) {
return this._rawValue.some(value => toggle.value != null && value === toggle.value);
}
return toggle.value === this._rawValue;
}
/** Updates the selection state of the toggles in the group based on a value. */
private _setSelectionByValue(value: any|any[])
{
this._rawValue = value;
if (!this._buttonToggles) {
return;
}
if (this.multiple && value) {
if (!Array.isArray(value) && (typeof ngDevMode === 'undefined' || ngDevMode)) {
throw Error('Value must be an array in multiple-selection mode.');
}
this._clearSelection();
value.forEach((currentValue: any) => this._selectValue(currentValue));
} else {
this._clearSelection();
this._selectValue(value);
}
}
/** Clears the selected toggles. */
private _clearSelection()
{
this._selectionModel.clear();
this._buttonToggles.forEach(toggle => (toggle.checked = false));
}
/** Selects a value if there's a toggle that corresponds to it. */
private _selectValue(value: any)
{
const correspondingOption = this._buttonToggles.find(toggle => {
return toggle.value != null && toggle.value === value;
});
if (correspondingOption) {
correspondingOption.checked = true;
this._selectionModel.select(correspondingOption);
}
}
/** Syncs up the group's value with the model and emits the change event. */
private _updateModelValue(isUserInput: boolean)
{
// Only emit the change event for user input.
if (isUserInput) {
this._emitChangeEvent();
}
// Note: we emit this one no twndter whether it was a user interaction, because
// it is used by Angular to sync up the two-way data binding.
this.valueChange.emit(this.value);
}
static ngAcceptInputType_disabled: BooleanInput;
static ngAcceptInputType_multiple: BooleanInput;
static ngAcceptInputType_vertical: BooleanInput;
}
// Boilerplate for applying mixins to the TWNDButtonToggle class.
/** @docs-private */
const _TWNDButtonToggleBase = mixinDisableRipple(class {});
/** Single button inside of a toggle group. */
@Component({
selector : 'twnd-button-toggle',
templateUrl : 'toggle.html',
encapsulation : ViewEncapsulation.None,
exportAs : 'twndButtonToggle',
changeDetection : ChangeDetectionStrategy.OnPush,
inputs : [ 'disableRipple' ],
host : {
'[class.twnd-button-toggle-standalone]' : '!buttonToggleGroup',
'[class.twnd-button-toggle-checked]' : 'checked',
'[class.twnd-button-toggle-disabled]' : 'disabled',
'[class.twnd-button-toggle-appearance-standard]' : 'appearance === "standard"',
'class' : 'twnd-button-toggle',
'[attr.aria-label]' : 'null',
'[attr.aria-labelledby]' : 'null',
'[attr.id]' : 'id',
'[attr.name]' : 'null',
'(focus)' : 'focus()',
'role' : 'presentation',
},
})
export class TWNDButtonToggle extends _TWNDButtonToggleBase implements OnInit, AfterViewInit,
CanDisableRipple, OnDestroy
{
private _isSingleSelector = false;
private _checked = false;
/**
* Attached to the aria-label attribute of the host element. In most cases, aria-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;
/** Underlying native `button` element. */
@ViewChild('button') _buttonElement: ElementRef<HTMLButtonElement>;
/** The parent button toggle group (exclusive selection). Optional. */
buttonToggleGroup: TWNDButtonToggleGroup;
/** Unique ID for the underlying `button` element. */
get buttonId(): string
{
return `${this.id}-button`;
}
/** The unique ID for this button toggle. */
@Input() id: string;
/** HTML's 'name' attribute used to group radios for unique selection. */
@Input() name: string;
/** TWNDButtonToggleGroup reads this to assign its own value. */
@Input() value: any;
/** Tabindex for the toggle. */
@Input() tabIndex: number|null;
/** The appearance style of the button. */
@Input() get appearance(): TWNDButtonToggleAppearance
{
return this.buttonToggleGroup ? this.buttonToggleGroup.appearance : this._appearance;
}
set appearance(value: TWNDButtonToggleAppearance)
{
this._appearance = value;
}
private _appearance: TWNDButtonToggleAppearance;
/** Whether the button is checked. */
@Input() get checked(): boolean
{
return this.buttonToggleGroup ? this.buttonToggleGroup._isSelected(this) : this._checked;
}
set checked(value: boolean)
{
const newValue = coerceBooleanProperty(value);
if (newValue !== this._checked) {
this._checked = newValue;
if (this.buttonToggleGroup) {
this.buttonToggleGroup._syncButtonToggle(this, this._checked);
}
this._changeDetectorRef.markForCheck();
}
}
/** Whether the button is disabled. */
@Input() get disabled(): boolean
{
return this._disabled || (this.buttonToggleGroup && this.buttonToggleGroup.disabled);
}
set disabled(value: boolean)
{
this._disabled = coerceBooleanProperty(value);
}
private _disabled: boolean = false;
/** Event emitted when the group value changes. */
@Output()
readonly change:
EventEmitter<TWNDButtonToggleChange> = new EventEmitter<TWNDButtonToggleChange>();
constructor(
@Optional() @Inject(TWND_BUTTON_TOGGLE_GROUP) toggleGroup: TWNDButtonToggleGroup,
private _changeDetectorRef: ChangeDetectorRef,
private _elementRef: ElementRef<HTMLElement>,
private _focusMonitor: FocusMonitor,
@Attribute('tabindex') defaultTabIndex: string,
@Optional() @Inject(TWND_BUTTON_TOGGLE_DEFAULT_OPTIONS) defaultOptions?:
TWNDButtonToggleDefaultOptions,
)
{
super();
const parsedTabIndex = Number(defaultTabIndex);
this.tabIndex = parsedTabIndex || parsedTabIndex === 0 ? parsedTabIndex : null;
this.buttonToggleGroup = toggleGroup;
this.appearance = defaultOptions && defaultOptions.appearance ? defaultOptions.appearance :
'standard';
}
ngOnInit()
{
const group = this.buttonToggleGroup;
this._isSingleSelector = group && !group.multiple;
this.id = this.id || `twnd-button-toggle-${uniqueIdCounter++}`;
if (this._isSingleSelector) {
this.name = group.name;
}
if (group) {
if (group._isPrechecked(this)) {
this.checked = true;
} else if (group._isSelected(this) !== this._checked) {
// As as side effect of the circular dependency between the toggle group and the button,
// we may end up in a state where the button is supposed to be checked on init, but it
// isn't, because the checked value was assigned too early. This can happen when Ivy
// assigns the static input value before the `ngOnInit` has run.
group._syncButtonToggle(this, this._checked);
}
}
}
ngAfterViewInit()
{
this._focusMonitor.monitor(this._elementRef, true);
}
ngOnDestroy()
{
const group = this.buttonToggleGroup;
this._focusMonitor.stopMonitoring(this._elementRef);
// Remove the toggle from the selection once it's destroyed. Needs to happen
// on the next tick in order to avoid "changed after checked" errors.
if (group && group._isSelected(this)) {
group._syncButtonToggle(this, false, false, true);
}
}
/** Focuses the button. */
focus(options?: FocusOptions): void
{
this._buttonElement.nativeElement.focus(options);
}
/** Checks the button toggle due to an interaction with the underlying native button. */
_onButtonClick()
{
const newChecked = this._isSingleSelector ? true : !this._checked;
if (newChecked !== this._checked) {
this._checked = newChecked;
if (this.buttonToggleGroup) {
this.buttonToggleGroup._syncButtonToggle(this, this._checked, true);
this.buttonToggleGroup._onTouched();
}
}
// Emit a change event when it's the single selector
this.change.emit(new TWNDButtonToggleChange(this, this.value));
}
/**
* 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 the group value changes, the button will not be notified.
// Use `markForCheck` to explicit update button toggle's status.
this._changeDetectorRef.markForCheck();
}
static ngAcceptInputType_checked: BooleanInput;
static ngAcceptInputType_disabled: BooleanInput;
static ngAcceptInputType_vertical: BooleanInput;
static ngAcceptInputType_multiple: BooleanInput;
static ngAcceptInputType_disableRipple: BooleanInput;
}