sass-references/angular-material/material/list/list-option.ts

343 lines
12 KiB
TypeScript
Raw Permalink Normal View History

2024-12-06 10:42:08 +08:00
/**
* @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.dev/license
*/
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
import {SelectionModel} from '@angular/cdk/collections';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ContentChildren,
ElementRef,
EventEmitter,
InjectionToken,
Input,
OnDestroy,
OnInit,
Output,
QueryList,
ViewChild,
ViewEncapsulation,
inject,
} from '@angular/core';
import {ThemePalette} from '@angular/material/core';
import {MatListBase, MatListItemBase} from './list-base';
import {LIST_OPTION, ListOption, MatListOptionTogglePosition} from './list-option-types';
import {MatListItemLine, MatListItemTitle} from './list-item-sections';
import {NgTemplateOutlet} from '@angular/common';
import {CdkObserveContent} from '@angular/cdk/observers';
/**
* Injection token that can be used to reference instances of an `SelectionList`. It serves
* as alternative token to an actual implementation which would result in circular references.
* @docs-private
*/
export const SELECTION_LIST = new InjectionToken<SelectionList>('SelectionList');
/**
* Interface describing the containing list of a list option. This is used to avoid
* circular dependencies between the list-option and the selection list.
* @docs-private
*/
export interface SelectionList extends MatListBase {
multiple: boolean;
color: ThemePalette;
selectedOptions: SelectionModel<MatListOption>;
hideSingleSelectionIndicator: boolean;
compareWith: (o1: any, o2: any) => boolean;
_value: string[] | null;
_reportValueChange(): void;
_emitChangeEvent(options: MatListOption[]): void;
_onTouched(): void;
}
@Component({
selector: 'mat-list-option',
exportAs: 'matListOption',
styleUrl: 'list-option.css',
host: {
'class': 'mat-mdc-list-item mat-mdc-list-option mdc-list-item',
'role': 'option',
// As per MDC, only list items without checkbox or radio indicator should receive the
// `--selected` class.
'[class.mdc-list-item--selected]':
'selected && !_selectionList.multiple && _selectionList.hideSingleSelectionIndicator',
// Based on the checkbox/radio position and whether there are icons or avatars, we apply MDC's
// list-item `--leading` and `--trailing` classes.
'[class.mdc-list-item--with-leading-avatar]': '_hasProjected("avatars", "before")',
'[class.mdc-list-item--with-leading-icon]': '_hasProjected("icons", "before")',
'[class.mdc-list-item--with-trailing-icon]': '_hasProjected("icons", "after")',
'[class.mat-mdc-list-option-with-trailing-avatar]': '_hasProjected("avatars", "after")',
// Based on the checkbox/radio position, we apply the `--leading` or `--trailing` MDC classes
// which ensure that the checkbox/radio is positioned correctly within the list item.
'[class.mdc-list-item--with-leading-checkbox]': '_hasCheckboxAt("before")',
'[class.mdc-list-item--with-trailing-checkbox]': '_hasCheckboxAt("after")',
'[class.mdc-list-item--with-leading-radio]': '_hasRadioAt("before")',
'[class.mdc-list-item--with-trailing-radio]': '_hasRadioAt("after")',
// Utility class that makes it easier to target the case where there's both a leading
// and a trailing icon. Avoids having to write out all the combinations.
'[class.mat-mdc-list-item-both-leading-and-trailing]': '_hasBothLeadingAndTrailing()',
'[class.mat-accent]': 'color !== "primary" && color !== "warn"',
'[class.mat-warn]': 'color === "warn"',
'[class._mat-animation-noopable]': '_noopAnimations',
'[attr.aria-selected]': 'selected',
'(blur)': '_handleBlur()',
'(click)': '_toggleOnInteraction()',
},
templateUrl: 'list-option.html',
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{provide: MatListItemBase, useExisting: MatListOption},
{provide: LIST_OPTION, useExisting: MatListOption},
],
imports: [NgTemplateOutlet, CdkObserveContent],
})
export class MatListOption extends MatListItemBase implements ListOption, OnInit, OnDestroy {
private _selectionList = inject<SelectionList>(SELECTION_LIST);
private _changeDetectorRef = inject(ChangeDetectorRef);
@ContentChildren(MatListItemLine, {descendants: true}) _lines: QueryList<MatListItemLine>;
@ContentChildren(MatListItemTitle, {descendants: true}) _titles: QueryList<MatListItemTitle>;
@ViewChild('unscopedContent') _unscopedContent: ElementRef<HTMLSpanElement>;
/**
* Emits when the selected state of the option has changed.
* Use to facilitate two-data binding to the `selected` property.
* @docs-private
*/
@Output()
readonly selectedChange: EventEmitter<boolean> = new EventEmitter<boolean>();
/** Whether the label should appear before or after the checkbox/radio. Defaults to 'after' */
@Input() togglePosition: MatListOptionTogglePosition = 'after';
/**
* Whether the label should appear before or after the checkbox/radio. Defaults to 'after'
*
* @deprecated Use `togglePosition` instead.
* @breaking-change 17.0.0
*/
@Input() get checkboxPosition(): MatListOptionTogglePosition {
return this.togglePosition;
}
set checkboxPosition(value: MatListOptionTogglePosition) {
this.togglePosition = value;
}
/**
* Theme color of the list option. This sets the color of the checkbox/radio.
* This API is supported in M2 themes only, it has no effect in M3 themes.
*
* For information on applying color variants in M3, see
* https://material.angular.io/guide/theming#using-component-color-variants.
*/
@Input()
get color(): ThemePalette {
return this._color || this._selectionList.color;
}
set color(newValue: ThemePalette) {
this._color = newValue;
}
private _color: ThemePalette;
/** Value of the option */
@Input()
get value(): any {
return this._value;
}
set value(newValue: any) {
if (this.selected && newValue !== this.value && this._inputsInitialized) {
this.selected = false;
}
this._value = newValue;
}
private _value: any;
/** Whether the option is selected. */
@Input()
get selected(): boolean {
return this._selectionList.selectedOptions.isSelected(this);
}
set selected(value: BooleanInput) {
const isSelected = coerceBooleanProperty(value);
if (isSelected !== this._selected) {
this._setSelected(isSelected);
if (isSelected || this._selectionList.multiple) {
this._selectionList._reportValueChange();
}
}
}
private _selected = false;
/**
* This is set to true after the first OnChanges cycle so we don't
* clear the value of `selected` in the first cycle.
*/
private _inputsInitialized = false;
ngOnInit() {
const list = this._selectionList;
if (list._value && list._value.some(value => list.compareWith(this._value, value))) {
this._setSelected(true);
}
const wasSelected = this._selected;
// List options that are selected at initialization can't be reported properly to the form
// control. This is because it takes some time until the selection-list knows about all
// available options. Also it can happen that the ControlValueAccessor has an initial value
// that should be used instead. Deferring the value change report to the next tick ensures
// that the form control value is not being overwritten.
Promise.resolve().then(() => {
if (this._selected || wasSelected) {
this.selected = true;
this._changeDetectorRef.markForCheck();
}
});
this._inputsInitialized = true;
}
override ngOnDestroy(): void {
super.ngOnDestroy();
if (this.selected) {
// We have to delay this until the next tick in order
// to avoid changed after checked errors.
Promise.resolve().then(() => {
this.selected = false;
});
}
}
/** Toggles the selection state of the option. */
toggle(): void {
this.selected = !this.selected;
}
/** Allows for programmatic focusing of the option. */
focus(): void {
this._hostElement.focus();
}
/** Gets the text label of the list option. Used for the typeahead functionality in the list. */
getLabel() {
const titleElement = this._titles?.get(0)?._elementRef.nativeElement;
// If there is no explicit title element, the unscoped text content
// is treated as the list item title.
const labelEl = titleElement || this._unscopedContent?.nativeElement;
return labelEl?.textContent || '';
}
/** Whether a checkbox is shown at the given position. */
_hasCheckboxAt(position: MatListOptionTogglePosition): boolean {
return this._selectionList.multiple && this._getTogglePosition() === position;
}
/** Where a radio indicator is shown at the given position. */
_hasRadioAt(position: MatListOptionTogglePosition): boolean {
return (
!this._selectionList.multiple &&
this._getTogglePosition() === position &&
!this._selectionList.hideSingleSelectionIndicator
);
}
/** Whether icons or avatars are shown at the given position. */
_hasIconsOrAvatarsAt(position: 'before' | 'after'): boolean {
return this._hasProjected('icons', position) || this._hasProjected('avatars', position);
}
/** Gets whether the given type of element is projected at the specified position. */
_hasProjected(type: 'icons' | 'avatars', position: 'before' | 'after'): boolean {
// If the checkbox/radio is shown at the specified position, neither icons or
// avatars can be shown at the position.
return (
this._getTogglePosition() !== position &&
(type === 'avatars' ? this._avatars.length !== 0 : this._icons.length !== 0)
);
}
_handleBlur() {
this._selectionList._onTouched();
}
/** Gets the current position of the checkbox/radio. */
_getTogglePosition() {
return this.togglePosition || 'after';
}
/**
* Sets the selected state of the option.
* @returns Whether the value has changed.
*/
_setSelected(selected: boolean): boolean {
if (selected === this._selected) {
return false;
}
this._selected = selected;
if (selected) {
this._selectionList.selectedOptions.select(this);
} else {
this._selectionList.selectedOptions.deselect(this);
}
this.selectedChange.emit(selected);
this._changeDetectorRef.markForCheck();
return true;
}
/**
* Notifies Angular that the option needs to be checked in the next change detection run.
* Mainly used to trigger an update of the list option if the disabled state of the selection
* list changed.
*/
_markForCheck() {
this._changeDetectorRef.markForCheck();
}
/** Toggles the option's value based on a user interaction. */
_toggleOnInteraction() {
if (!this.disabled) {
if (this._selectionList.multiple) {
this.selected = !this.selected;
this._selectionList._emitChangeEvent([this]);
} else if (!this.selected) {
this.selected = true;
this._selectionList._emitChangeEvent([this]);
}
}
}
/** Sets the tabindex of the list option. */
_setTabindex(value: number) {
this._hostElement.setAttribute('tabindex', value + '');
}
protected _hasBothLeadingAndTrailing(): boolean {
const hasLeading =
this._hasProjected('avatars', 'before') ||
this._hasProjected('icons', 'before') ||
this._hasCheckboxAt('before') ||
this._hasRadioAt('before');
const hasTrailing =
this._hasProjected('icons', 'after') ||
this._hasProjected('avatars', 'after') ||
this._hasCheckboxAt('after') ||
this._hasRadioAt('after');
return hasLeading && hasTrailing;
}
}