/
githubmirror
/
components
Обзор
Документация
Войти
/
githubmirror
/
components
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/material/chips/chip-row.ts
246 строк
8 KB
Kristiyan Kostadinov
refactor(material/chips): remove explicit change detection
09 май 2026, 06:37
09 май 2026, 06:37
1e3b18d
Код
Авторство
О чём код?
/** * @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, Component, ContentChild, EventEmitter, Input, OnDestroy, Output, Renderer2, ViewChild, ViewEncapsulation, afterNextRender, inject, } 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-leading-action]': '_hasLeadingActionIcon()', '[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()', '(click)': 'this._hasInteractiveActions() ? _handleClick($event) : null', '(dblclick)': '_handleDoubleclick($event)', }, providers: [ {provide: MatChip, useExisting: MatChipRow}, {provide: MAT_CHIP, useExisting: MatChipRow}, ], encapsulation: ViewEncapsulation.None, imports: [MatChipAction, MatChipEditInput], }) export class MatChipRow extends MatChip implements AfterViewInit, OnDestroy { protected override basicChipAttrName = 'mat-basic-chip-row'; private _renderer = inject(Renderer2); private _cleanupMousedown: (() => void) | undefined; /** * 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; /** * Set on a mousedown when the chip is already focused via mouse or keyboard. * * This allows us to ensure chip is already focused when deciding whether to enter the * edit mode on a subsequent click. Otherwise, the chip appears focused when handling the * first click event. */ private _alreadyFocused = false; _isEditing = false; constructor() { super(); this.role = 'row'; this._onBlur.pipe(takeUntil(this.destroyed)).subscribe(() => { if (this._isEditing && !this._editStartPending) { this._onEditFinish(); } this._alreadyFocused = false; }); } override ngAfterViewInit() { super.ngAfterViewInit(); // Sets _alreadyFocused (ahead of click) when chip already has focus. this._cleanupMousedown = this._ngZone.runOutsideAngular(() => this._renderer.listen(this._elementRef.nativeElement, 'mousedown', () => { this._alreadyFocused = this._hasFocus(); }), ); } override ngOnDestroy(): void { super.ngOnDestroy(); this._cleanupMousedown?.(); } protected _hasLeadingActionIcon() { // The leading action (edit) icon is hidden while editing. return !this._isEditing && !!this.editIcon; } 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); } } _handleClick(event: MouseEvent) { if (!this.disabled && this.editable && !this._isEditing && this._alreadyFocused) { // Ensure click event not picked up unintentionally by other listeners, as // once editing starts, the source element is detached from DOM. event.preventDefault(); event.stopPropagation(); this._startEditing(event); } } _handleDoubleclick(event: MouseEvent) { if (!this.disabled && this.editable) { this._startEditing(event); } } override _edit(): void { // markForCheck necessary for edit input to be rendered this._changeDetectorRef.markForCheck(); this._startEditing(); } private _startEditing(event?: Event) { if ( !this.primaryAction || (this.removeIcon && !!event && 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); // Necessary when using edit icon to prevent edit from aborting setTimeout(() => this._ngZone.run(() => (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!; } }