193 lines
6.0 KiB
TypeScript
193 lines
6.0 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 {ENTER} from '@angular/cdk/keycodes';
|
|
import {
|
|
AfterViewInit,
|
|
ChangeDetectionStrategy,
|
|
Component,
|
|
ContentChild,
|
|
EventEmitter,
|
|
Input,
|
|
Output,
|
|
ViewChild,
|
|
ViewEncapsulation,
|
|
afterNextRender,
|
|
} from '@angular/core';
|
|
import {takeUntil} from 'rxjs/operators';
|
|
import {MatChip, MatChipEvent} from './chip';
|
|
import {MatChipAction} from './chip-action';
|
|
import {MatChipEditInput} from './chip-edit-input';
|
|
import {MAT_CHIP} from './tokens';
|
|
|
|
/** Represents an event fired on an individual `mat-chip` when it is edited. */
|
|
export interface MatChipEditedEvent extends MatChipEvent {
|
|
/** The final edit value. */
|
|
value: string;
|
|
}
|
|
|
|
/**
|
|
* An extension of the MatChip component used with MatChipGrid and
|
|
* the matChipInputFor directive.
|
|
*/
|
|
@Component({
|
|
selector: 'mat-chip-row, [mat-chip-row], mat-basic-chip-row, [mat-basic-chip-row]',
|
|
templateUrl: 'chip-row.html',
|
|
styleUrl: 'chip.css',
|
|
host: {
|
|
'class': 'mat-mdc-chip mat-mdc-chip-row mdc-evolution-chip',
|
|
'[class.mat-mdc-chip-with-avatar]': 'leadingIcon',
|
|
'[class.mat-mdc-chip-disabled]': 'disabled',
|
|
'[class.mat-mdc-chip-editing]': '_isEditing',
|
|
'[class.mat-mdc-chip-editable]': 'editable',
|
|
'[class.mdc-evolution-chip--disabled]': 'disabled',
|
|
'[class.mdc-evolution-chip--with-trailing-action]': '_hasTrailingIcon()',
|
|
'[class.mdc-evolution-chip--with-primary-graphic]': 'leadingIcon',
|
|
'[class.mdc-evolution-chip--with-primary-icon]': 'leadingIcon',
|
|
'[class.mdc-evolution-chip--with-avatar]': 'leadingIcon',
|
|
'[class.mat-mdc-chip-highlighted]': 'highlighted',
|
|
'[class.mat-mdc-chip-with-trailing-icon]': '_hasTrailingIcon()',
|
|
'[id]': 'id',
|
|
// Has to have a negative tabindex in order to capture
|
|
// focus and redirect it to the primary action.
|
|
'[attr.tabindex]': 'disabled ? null : -1',
|
|
'[attr.aria-label]': 'null',
|
|
'[attr.aria-description]': 'null',
|
|
'[attr.role]': 'role',
|
|
'(focus)': '_handleFocus($event)',
|
|
'(dblclick)': '_handleDoubleclick($event)',
|
|
},
|
|
providers: [
|
|
{provide: MatChip, useExisting: MatChipRow},
|
|
{provide: MAT_CHIP, useExisting: MatChipRow},
|
|
],
|
|
encapsulation: ViewEncapsulation.None,
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
imports: [MatChipAction, MatChipEditInput],
|
|
})
|
|
export class MatChipRow extends MatChip implements AfterViewInit {
|
|
protected override basicChipAttrName = 'mat-basic-chip-row';
|
|
|
|
/**
|
|
* The editing action has to be triggered in a timeout. While we're waiting on it, a blur
|
|
* event might occur which will interrupt the editing. This flag is used to avoid interruptions
|
|
* while the editing action is being initialized.
|
|
*/
|
|
private _editStartPending = false;
|
|
|
|
@Input() editable: boolean = false;
|
|
|
|
/** Emitted when the chip is edited. */
|
|
@Output() readonly edited: EventEmitter<MatChipEditedEvent> =
|
|
new EventEmitter<MatChipEditedEvent>();
|
|
|
|
/** The default chip edit input that is used if none is projected into this chip row. */
|
|
@ViewChild(MatChipEditInput) defaultEditInput?: MatChipEditInput;
|
|
|
|
/** The projected chip edit input. */
|
|
@ContentChild(MatChipEditInput) contentEditInput?: MatChipEditInput;
|
|
|
|
_isEditing = false;
|
|
|
|
constructor(...args: unknown[]);
|
|
|
|
constructor() {
|
|
super();
|
|
|
|
this.role = 'row';
|
|
this._onBlur.pipe(takeUntil(this.destroyed)).subscribe(() => {
|
|
if (this._isEditing && !this._editStartPending) {
|
|
this._onEditFinish();
|
|
}
|
|
});
|
|
}
|
|
|
|
override _hasTrailingIcon() {
|
|
// The trailing icon is hidden while editing.
|
|
return !this._isEditing && super._hasTrailingIcon();
|
|
}
|
|
|
|
/** Sends focus to the first gridcell when the user clicks anywhere inside the chip. */
|
|
_handleFocus() {
|
|
if (!this._isEditing && !this.disabled) {
|
|
this.focus();
|
|
}
|
|
}
|
|
|
|
override _handleKeydown(event: KeyboardEvent): void {
|
|
if (event.keyCode === ENTER && !this.disabled) {
|
|
if (this._isEditing) {
|
|
event.preventDefault();
|
|
this._onEditFinish();
|
|
} else if (this.editable) {
|
|
this._startEditing(event);
|
|
}
|
|
} else if (this._isEditing) {
|
|
// Stop the event from reaching the chip set in order to avoid navigating.
|
|
event.stopPropagation();
|
|
} else {
|
|
super._handleKeydown(event);
|
|
}
|
|
}
|
|
|
|
_handleDoubleclick(event: MouseEvent) {
|
|
if (!this.disabled && this.editable) {
|
|
this._startEditing(event);
|
|
}
|
|
}
|
|
|
|
private _startEditing(event: Event) {
|
|
if (
|
|
!this.primaryAction ||
|
|
(this.removeIcon && this._getSourceAction(event.target as Node) === this.removeIcon)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
// The value depends on the DOM so we need to extract it before we flip the flag.
|
|
const value = this.value;
|
|
|
|
this._isEditing = this._editStartPending = true;
|
|
|
|
// Defer initializing the input until after it has been added to the DOM.
|
|
afterNextRender(
|
|
() => {
|
|
this._getEditInput().initialize(value);
|
|
this._editStartPending = false;
|
|
},
|
|
{injector: this._injector},
|
|
);
|
|
}
|
|
|
|
private _onEditFinish() {
|
|
this._isEditing = this._editStartPending = false;
|
|
this.edited.emit({chip: this, value: this._getEditInput().getValue()});
|
|
|
|
// If the edit input is still focused or focus was returned to the body after it was destroyed,
|
|
// return focus to the chip contents.
|
|
if (
|
|
this._document.activeElement === this._getEditInput().getNativeElement() ||
|
|
this._document.activeElement === this._document.body
|
|
) {
|
|
this.primaryAction.focus();
|
|
}
|
|
}
|
|
|
|
override _isRippleDisabled(): boolean {
|
|
return super._isRippleDisabled() || this._isEditing;
|
|
}
|
|
|
|
/**
|
|
* Gets the projected chip edit input, or the default input if none is projected in. One of these
|
|
* two values is guaranteed to be defined.
|
|
*/
|
|
private _getEditInput(): MatChipEditInput {
|
|
return this.contentEditInput || this.defaultEditInput!;
|
|
}
|
|
}
|