227 lines
6.6 KiB
TypeScript
227 lines
6.6 KiB
TypeScript
/**
|
|
* @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);
|
|
}
|
|
}
|