sass-references/angular-material/material/chips/chip-input.ts

227 lines
6.6 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 {BACKSPACE, hasModifierKey} from '@angular/cdk/keycodes';
import {
Directive,
ElementRef,
EventEmitter,
Input,
OnChanges,
OnDestroy,
Output,
booleanAttribute,
inject,
} from '@angular/core';
import {_IdGenerator} from '@angular/cdk/a11y';
import {MatFormField, MAT_FORM_FIELD} from '@angular/material/form-field';
import {MatChipsDefaultOptions, MAT_CHIPS_DEFAULT_OPTIONS} from './tokens';
import {MatChipGrid} from './chip-grid';
import {MatChipTextControl} from './chip-text-control';
/** Represents an input event on a `matChipInput`. */
export interface MatChipInputEvent {
/**
* The native `<input>` element that the event is being fired for.
* @deprecated Use `MatChipInputEvent#chipInput.inputElement` instead.
* @breaking-change 13.0.0 This property will be removed.
*/
input: HTMLInputElement;
/** The value of the input. */
value: string;
/** Reference to the chip input that emitted the event. */
chipInput: MatChipInput;
}
/**
* Directive that adds chip-specific behaviors to an input element inside `<mat-form-field>`.
* May be placed inside or outside of a `<mat-chip-grid>`.
*/
@Directive({
selector: 'input[matChipInputFor]',
exportAs: 'matChipInput, matChipInputFor',
host: {
// TODO: eventually we should remove `mat-input-element` from here since it comes from the
// non-MDC version of the input. It's currently being kept for backwards compatibility, because
// the MDC chips were landed initially with it.
'class': 'mat-mdc-chip-input mat-mdc-input-element mdc-text-field__input mat-input-element',
'(keydown)': '_keydown($event)',
'(blur)': '_blur()',
'(focus)': '_focus()',
'(input)': '_onInput()',
'[id]': 'id',
'[attr.disabled]': 'disabled || null',
'[attr.placeholder]': 'placeholder || null',
'[attr.aria-invalid]': '_chipGrid && _chipGrid.ngControl ? _chipGrid.ngControl.invalid : null',
'[attr.aria-required]': '_chipGrid && _chipGrid.required || null',
'[attr.required]': '_chipGrid && _chipGrid.required || null',
},
})
export class MatChipInput implements MatChipTextControl, OnChanges, OnDestroy {
protected _elementRef = inject<ElementRef<HTMLInputElement>>(ElementRef);
/** Whether the control is focused. */
focused: boolean = false;
/** Register input for chip list */
@Input('matChipInputFor')
get chipGrid(): MatChipGrid {
return this._chipGrid;
}
set chipGrid(value: MatChipGrid) {
if (value) {
this._chipGrid = value;
this._chipGrid.registerInput(this);
}
}
private _chipGrid: MatChipGrid;
/**
* Whether or not the chipEnd event will be emitted when the input is blurred.
*/
@Input({alias: 'matChipInputAddOnBlur', transform: booleanAttribute})
addOnBlur: boolean = false;
/**
* The list of key codes that will trigger a chipEnd event.
*
* Defaults to `[ENTER]`.
*/
@Input('matChipInputSeparatorKeyCodes')
separatorKeyCodes: readonly number[] | ReadonlySet<number>;
/** Emitted when a chip is to be added. */
@Output('matChipInputTokenEnd')
readonly chipEnd: EventEmitter<MatChipInputEvent> = new EventEmitter<MatChipInputEvent>();
/** The input's placeholder text. */
@Input() placeholder: string = '';
/** Unique id for the input. */
@Input() id: string = inject(_IdGenerator).getId('mat-mdc-chip-list-input-');
/** Whether the input is disabled. */
@Input({transform: booleanAttribute})
get disabled(): boolean {
return this._disabled || (this._chipGrid && this._chipGrid.disabled);
}
set disabled(value: boolean) {
this._disabled = value;
}
private _disabled: boolean = false;
/** Whether the input is empty. */
get empty(): boolean {
return !this.inputElement.value;
}
/** The native input element to which this directive is attached. */
readonly inputElement!: HTMLInputElement;
constructor(...args: unknown[]);
constructor() {
const defaultOptions = inject<MatChipsDefaultOptions>(MAT_CHIPS_DEFAULT_OPTIONS);
const formField = inject<MatFormField>(MAT_FORM_FIELD, {optional: true});
this.inputElement = this._elementRef.nativeElement as HTMLInputElement;
this.separatorKeyCodes = defaultOptions.separatorKeyCodes;
if (formField) {
this.inputElement.classList.add('mat-mdc-form-field-input-control');
}
}
ngOnChanges() {
this._chipGrid.stateChanges.next();
}
ngOnDestroy(): void {
this.chipEnd.complete();
}
/** Utility method to make host definition/tests more clear. */
_keydown(event: KeyboardEvent) {
if (this.empty && event.keyCode === BACKSPACE) {
// Ignore events where the user is holding down backspace
// so that we don't accidentally remove too many chips.
if (!event.repeat) {
this._chipGrid._focusLastChip();
}
event.preventDefault();
} else {
this._emitChipEnd(event);
}
}
/** Checks to see if the blur should emit the (chipEnd) event. */
_blur() {
if (this.addOnBlur) {
this._emitChipEnd();
}
this.focused = false;
// Blur the chip list if it is not focused
if (!this._chipGrid.focused) {
this._chipGrid._blur();
}
this._chipGrid.stateChanges.next();
}
_focus() {
this.focused = true;
this._chipGrid.stateChanges.next();
}
/** Checks to see if the (chipEnd) event needs to be emitted. */
_emitChipEnd(event?: KeyboardEvent) {
if (!event || (this._isSeparatorKey(event) && !event.repeat)) {
this.chipEnd.emit({
input: this.inputElement,
value: this.inputElement.value,
chipInput: this,
});
event?.preventDefault();
}
}
_onInput() {
// Let chip list know whenever the value changes.
this._chipGrid.stateChanges.next();
}
/** Focuses the input. */
focus(): void {
this.inputElement.focus();
}
/** Clears the input */
clear(): void {
this.inputElement.value = '';
}
setDescribedByIds(ids: string[]): void {
const element = this._elementRef.nativeElement;
// Set the value directly in the DOM since this binding
// is prone to "changed after checked" errors.
if (ids.length) {
element.setAttribute('aria-describedby', ids.join(' '));
} else {
element.removeAttribute('aria-describedby');
}
}
/** Checks whether a keycode is one of the configured separators. */
private _isSeparatorKey(event: KeyboardEvent) {
return !hasModifierKey(event) && new Set(this.separatorKeyCodes).has(event.keyCode);
}
}