@progress/kendo-angular-inputs
Version:
Kendo UI for Angular Inputs Package - Everything you need to build professional form functionality (Checkbox, ColorGradient, ColorPalette, ColorPicker, FlatColorPicker, FormField, MaskedTextBox, NumericTextBox, RadioButton, RangeSlider, Slider, Switch, Te
757 lines (756 loc) • 28.3 kB
JavaScript
/**-----------------------------------------------------------------------------------------
* Copyright © 2025 Progress Software Corporation. All rights reserved.
* Licensed under commercial license. See LICENSE.md in the project root for more information
*-------------------------------------------------------------------------------------------*/
import { Component, HostBinding, Input, Output, ElementRef, EventEmitter, NgZone, Renderer2, ChangeDetectorRef, Injector, forwardRef, ContentChild } from '@angular/core';
import { fromEvent } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
import { NG_VALUE_ACCESSOR, NgControl } from '@angular/forms';
import { KendoInput, guid, isDocumentAvailable, hasObservers, setHTMLAttributes, isControlRequired, isObjectPresent, removeHTMLAttributes, parseAttributes, EventsOutsideAngularDirective } from '@progress/kendo-angular-common';
import { LocalizationService, L10N_PREFIX } from '@progress/kendo-angular-l10n';
import { TextFieldsBase } from '../text-fields-common/text-fields-base';
import { areSame, isPresent, getStylingClasses } from '../common/utils';
import { invokeElementMethod } from '../common/dom-utils';
import { validatePackage } from '@progress/kendo-licensing';
import { packageMetadata } from '../package-metadata';
import { TextAreaPrefixComponent } from './textarea-prefix.component';
import { TextAreaSuffixComponent } from './textarea-suffix.component';
import { InputSeparatorComponent } from '../shared/input-separator.component';
import { NgIf, NgClass } from '@angular/common';
import { SharedInputEventsDirective } from '../shared/shared-events.directive';
import * as i0 from "@angular/core";
import * as i1 from "@progress/kendo-angular-l10n";
const resizeClasses = {
'vertical': 'k-resize-y',
'horizontal': 'k-resize-x',
'both': 'k-resize',
'none': 'k-resize-none',
'auto': 'k-resize-none'
};
const FOCUSED = 'k-focus';
const DEFAULT_SIZE = 'medium';
const DEFAULT_ROUNDED = 'medium';
const DEFAULT_FILL_MODE = 'solid';
/**
* Represents the [Kendo UI TextArea component for Angular]({% slug overview_textarea %}).
*/
export class TextAreaComponent extends TextFieldsBase {
localizationService;
ngZone;
changeDetector;
renderer;
injector;
hostElement;
/**
* @hidden
*/
focusableId = `k-${guid()}`;
hostClasses = true;
get flowCol() {
return this.flow === 'vertical';
}
get flowRow() {
return this.flow === 'horizontal';
}
_flow = 'vertical';
/**
* Specifies the flow direction of the TextArea sections. This property is useful when adornments are used, in order to specify
* their position in relation to the textarea element.
*
* The possible values are:
* * `vertical`(Default) —TextArea sections are placed from top to bottom.
* * `horizontal`—TextArea sections are placed from left to right in `ltr`, and from right to left in `rtl` mode.
*/
set flow(flow) {
this._flow = flow;
if (this.prefix) {
this.prefix.flow = flow;
}
if (this.suffix) {
this.suffix.flow = flow;
}
}
get flow() {
return this._flow;
}
/**
* Sets the HTML attributes of the inner focusable input element. Attributes which are essential for certain component functionalities cannot be changed.
*/
set inputAttributes(attributes) {
if (isObjectPresent(this.parsedAttributes)) {
removeHTMLAttributes(this.parsedAttributes, this.renderer, this.input.nativeElement);
}
this._inputAttributes = attributes;
this.parsedAttributes = this.inputAttributes ?
parseAttributes(this.inputAttributes, this.defaultAttributes) :
this.inputAttributes;
this.setInputAttributes();
}
get inputAttributes() {
return this._inputAttributes;
}
/**
* Specifies the orientation of the TextArea adornments. This property is used in order to specify
* the adornments' position relative to themselves.
*
* The possible values are:
* * `horizontal`(Default) —TextArea adornments are placed from left to right in `ltr`, and from right to left in `rtl` mode.
* * `vertical`—TextArea adornments are placed from top to bottom.
*/
set adornmentsOrientation(orientation) {
this._adornmentsOrientation = orientation;
if (this.prefix) {
this.prefix.orientation = orientation;
}
if (this.suffix) {
this.suffix.orientation = orientation;
}
}
get adornmentsOrientation() {
return this._adornmentsOrientation;
}
/**
* Specifies the visible height of the textarea element in lines.
*/
rows;
/**
* Specifies the visible width of the textarea element (in average character width).
*/
cols;
/**
* Specifies the maximum number of characters that the user can enter in the TextArea component.
*/
maxlength;
/**
* Specifies the [tabindex](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex) of the component.
*/
tabindex = 0;
/**
* @hidden
*/
set tabIndex(tabIndex) {
this.tabindex = tabIndex;
}
get tabIndex() {
return this.tabindex;
}
/**
* Configures the resize behavior of the TextArea.
*
* The possible values are:
* * `vertical`(Default)—The TextArea component can be resized only vertically.
* * `horizontal`—The TextArea component can be resized only horizontally.
* * `both`—The TextArea component can be resized in both (horizontal and vertical) directions.
* * `auto`—Specifies whether the TextArea component will adjust its height automatically, based on the content.
* * `none`—The TextArea cannot be resized.
*
*/
resizable = 'vertical';
/**
* The size property specifies the padding of the internal textarea element
* ([see example]({% slug appearance_textarea %}#toc-size)).
*
* The possible values are:
* * `small`
* * `medium` (default)
* * `large`
* * `none`
*/
set size(size) {
const newSize = size ? size : DEFAULT_SIZE;
this.handleClasses(newSize, 'size');
this._size = newSize;
}
get size() {
return this._size;
}
/**
* The `rounded` property specifies the border radius of the TextArea
* ([see example](slug:appearance_textarea#toc-roundness)).
*
* The possible values are:
* * `small`
* * `medium` (default)
* * `large`
* * `none`
*/
set rounded(rounded) {
const newRounded = rounded ? rounded : DEFAULT_ROUNDED;
this.handleClasses(newRounded, 'rounded');
this._rounded = newRounded;
}
get rounded() {
return this._rounded;
}
/**
* The `fillMode` property specifies the background and border styles of the TextArea
* ([see example](slug:appearance_textarea#toc-fill-mode)).
*
* The possible values are:
* * `flat`
* * `solid` (default)
* * `outline`
* * `none`
*/
set fillMode(fillMode) {
const newFillMode = fillMode ? fillMode : DEFAULT_FILL_MODE;
this.handleClasses(newFillMode, 'fillMode');
this._fillMode = newFillMode;
}
get fillMode() {
return this._fillMode;
}
/**
* Specifies whether the prefix separator of the TextArea is rendered.
* If a prefix template is not declared, the separator will not be rendered, regardless of the parameter value.
*
* @default false
*/
showPrefixSeparator = false;
/**
* Specifies whether the suffix separator of the TextArea is rendered.
* If a suffix template is not declared, the separator will not be rendered, regardless of the parameter value.
*
* @default false
*/
showSuffixSeparator = false;
/**
* Fires each time the user focuses the TextArea component.
*
* > To wire the event programmatically, use the `onFocus` property.
*
* @example
* ```ts
* _@Component({
* selector: 'my-app',
* template: `
* <kendo-textarea (focus)="handleFocus()"></kendo-textarea>
* `
* })
* class AppComponent {
* public handleFocus(): void {
* console.log('Component is focused.');
* }
* }
* ```
*/
onFocus = new EventEmitter();
/**
* Fires each time the TextArea component gets blurred.
*
* > To wire the event programmatically, use the `onBlur` property.
*
* @example
* ```ts
* _@Component({
* selector: 'my-app',
* template: `
* <kendo-textarea (blur)="handleBlur()"></kendo-textarea>
* `
* })
* class AppComponent {
* public handleBlur(): void {
* console.log('Component is blurred');
* }
* }
* ```
*/
onBlur = new EventEmitter();
/**
* Fires each time the value is changed or the component is blurred
* ([see example](slug:events_textarea)).
* When the component value is changed programmatically or via its form control binding, the valueChange event is not emitted.
*/
valueChange = new EventEmitter();
initialHeight;
resizeSubscription;
_size = 'medium';
_rounded = 'medium';
_fillMode = 'solid';
_adornmentsOrientation = 'horizontal';
_inputAttributes;
parsedAttributes = {};
get defaultAttributes() {
return {
id: this.focusableId,
disabled: this.disabled ? '' : null,
readonly: this.readonly ? '' : null,
tabindex: this.disabled ? undefined : this.tabIndex,
placeholder: this.placeholder,
title: this.title,
maxlength: this.maxlength,
rows: this.rows,
cols: this.cols,
'aria-disabled': this.disabled ? true : undefined,
'aria-readonly': this.readonly ? true : undefined,
'aria-invalid': this.isControlInvalid,
required: this.isControlRequired ? '' : null
};
}
get mutableAttributes() {
return {
'aria-multiline': 'true'
};
}
constructor(localizationService, ngZone, changeDetector, renderer, injector, hostElement) {
super(localizationService, ngZone, changeDetector, renderer, injector, hostElement);
this.localizationService = localizationService;
this.ngZone = ngZone;
this.changeDetector = changeDetector;
this.renderer = renderer;
this.injector = injector;
this.hostElement = hostElement;
validatePackage(packageMetadata);
this.direction = localizationService.rtl ? 'rtl' : 'ltr';
}
ngAfterViewInit() {
this.ngZone.runOutsideAngular(() => {
this.handleFlow();
});
const stylingInputs = ['size', 'rounded', 'fillMode'];
stylingInputs.forEach(input => {
this.handleClasses(this[input], input);
});
}
ngOnInit() {
this.control = this.injector.get(NgControl, null);
if (isDocumentAvailable() && this.resizable === 'auto') {
this.resizeSubscription = fromEvent(window, 'resize')
.pipe((debounceTime(50)))
.subscribe(() => this.resize());
}
if (this.hostElement) {
this.renderer.removeAttribute(this.hostElement.nativeElement, "tabindex");
}
this.subscriptions = this.localizationService.changes.subscribe(({ rtl }) => {
this.direction = rtl ? 'rtl' : 'ltr';
});
}
ngOnChanges(changes) {
const hostElement = this.hostElement.nativeElement;
const element = this.input.nativeElement;
if (changes.flow) {
this.handleFlow();
}
if (changes.resizable) {
if (this.resizable === 'auto') {
this.renderer.removeClass(element, '\!k-overflow-y-auto');
this.initialHeight = element.offsetHeight;
}
else if (this.resizable !== 'both') {
this.renderer.addClass(element, '\!k-overflow-y-auto');
element.style.height = `${this.initialHeight}px`;
}
}
if (changes.cols) {
if (isPresent(changes.cols.currentValue)) {
this.renderer.setStyle(hostElement, 'width', 'auto');
}
else {
this.renderer.removeStyle(hostElement, 'width');
}
}
if (changes.value) {
this.resize();
}
}
/**
* @hidden
*/
prefix;
/**
* @hidden
*/
suffix;
/**
* @hidden
*/
writeValue(value) {
this.value = value;
this.resize();
this.changeDetector.markForCheck();
}
/**
* @hidden
*/
registerOnChange(fn) {
this.ngChange = fn;
}
/**
* @hidden
*/
registerOnTouched(fn) {
this.ngTouched = fn;
}
updateValue(value) {
if (!areSame(this.value, value)) {
this.ngZone.run(() => {
this.value = value;
this.ngChange(value);
this.valueChange.emit(value);
this.changeDetector.markForCheck();
});
}
}
ngOnDestroy() {
super.ngOnDestroy();
if (this.resizeSubscription) {
this.resizeSubscription.unsubscribe();
}
}
/**
* @hidden
*/
get resizableClass() {
return resizeClasses[this.resizable];
}
/**
* @hidden
*/
get isControlInvalid() {
return this.control && this.control.touched && !this.control.valid;
}
/**
* @hidden
*/
get isControlRequired() {
return isControlRequired(this.control?.control);
}
/**
* @hidden
*/
get separatorOrientation() {
return this.flow === 'horizontal' ? 'vertical' : 'horizontal';
}
/**
* @hidden
*/
get isFocused() {
return this._isFocused;
}
/**
* @hidden
*/
set isFocused(value) {
if (this._isFocused !== value && this.hostElement) {
const element = this.hostElement.nativeElement;
if (value && !this.disabled) {
this.renderer.addClass(element, FOCUSED);
}
else {
this.renderer.removeClass(element, FOCUSED);
}
this._isFocused = value;
}
}
/**
* @hidden
*/
handleInput = (ev) => {
const incomingValue = ev.target.value;
this.updateValue(incomingValue);
this.resize();
};
/**
* @hidden
*/
handleInputFocus = () => {
if (!this.disabled) {
if (this.selectOnFocus && this.value) {
this.ngZone.run(() => {
setTimeout(() => { this.selectAll(); });
});
}
if (!this.isFocused) {
this.handleFocus();
}
if (hasObservers(this.inputFocus)) {
if (!this.focusChangedProgrammatically) {
this.ngZone.run(() => {
this.inputFocus.emit();
});
}
}
}
};
/**
* Focuses the TextArea component.
*
* @example
* ```ts
* _@Component({
* selector: 'my-app',
* template: `
* <button (click)="textarea.focus()">Focus the textarea</button>
* <kendo-textarea #textarea></kendo-textarea>
* `
* })
* class AppComponent { }
* ```
*/
focus() {
if (!this.input) {
return;
}
this.focusChangedProgrammatically = true;
this.isFocused = true;
this.input.nativeElement.focus();
this.focusChangedProgrammatically = false;
}
/**
* Blurs the TextArea component.
*/
blur() {
this.focusChangedProgrammatically = true;
const isFocusedElement = this.hostElement.nativeElement.querySelector(':focus');
if (isFocusedElement) {
isFocusedElement.blur();
}
this.isFocused = false;
this.focusChangedProgrammatically = false;
}
resize() {
if (this.resizable !== 'auto') {
return;
}
// The logic of the resize method, does not depend on Angular and thus moving it outisde of it
// We need to ensure that the resizing logic runs after the value is updated thus the setTimout
this.ngZone.runOutsideAngular(() => {
setTimeout(() => {
const hostElement = this.hostElement.nativeElement;
const element = this.input.nativeElement;
this.renderer.setStyle(element, 'height', `${this.initialHeight}px`);
const scrollHeight = element.scrollHeight;
this.renderer.setStyle(hostElement, 'min-height', `${scrollHeight}px`);
if (scrollHeight > this.initialHeight) {
this.renderer.setStyle(element, 'height', `${scrollHeight}px`);
}
}, 0);
});
}
/**
* @hidden
*/
handleFocus() {
this.ngZone.run(() => {
if (!this.focusChangedProgrammatically && hasObservers(this.onFocus)) {
this.onFocus.emit();
}
this.isFocused = true;
});
}
/**
* @hidden
*/
handleBlur() {
this.changeDetector.markForCheck();
this.ngZone.run(() => {
if (!this.focusChangedProgrammatically) {
this.onBlur.emit();
}
this.isFocused = false;
});
}
setSelection(start, end) {
if (this.isFocused) {
invokeElementMethod(this.input, 'setSelectionRange', start, end);
}
}
selectAll() {
if (this.value) {
this.setSelection(0, this.value.length);
}
}
handleClasses(value, input) {
const elem = this.hostElement.nativeElement;
const classes = getStylingClasses('input', input, this[input], value);
if (classes.toRemove) {
this.renderer.removeClass(elem, classes.toRemove);
}
if (classes.toAdd) {
this.renderer.addClass(elem, classes.toAdd);
}
}
handleFlow() {
const isVertical = this.flow === 'vertical';
const element = this.input.nativeElement;
this.renderer[isVertical ? 'addClass' : 'removeClass'](element, '\!k-flex-none');
}
setInputAttributes() {
const attributesToRender = Object.assign({}, this.mutableAttributes, this.parsedAttributes);
setHTMLAttributes(attributesToRender, this.renderer, this.input.nativeElement, this.ngZone);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: TextAreaComponent, deps: [{ token: i1.LocalizationService }, { token: i0.NgZone }, { token: i0.ChangeDetectorRef }, { token: i0.Renderer2 }, { token: i0.Injector }, { token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: TextAreaComponent, isStandalone: true, selector: "kendo-textarea", inputs: { focusableId: "focusableId", flow: "flow", inputAttributes: "inputAttributes", adornmentsOrientation: "adornmentsOrientation", rows: "rows", cols: "cols", maxlength: "maxlength", tabindex: "tabindex", tabIndex: "tabIndex", resizable: "resizable", size: "size", rounded: "rounded", fillMode: "fillMode", showPrefixSeparator: "showPrefixSeparator", showSuffixSeparator: "showSuffixSeparator" }, outputs: { onFocus: "focus", onBlur: "blur", valueChange: "valueChange" }, host: { properties: { "class.k-textarea": "this.hostClasses", "class.k-input": "this.hostClasses", "class.!k-flex-col": "this.flowCol", "class.!k-flex-row": "this.flowRow" } }, providers: [
LocalizationService,
{ provide: L10N_PREFIX, useValue: 'kendo.textarea' },
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TextAreaComponent),
multi: true
},
{ provide: KendoInput, useExisting: forwardRef(() => TextAreaComponent) }
], queries: [{ propertyName: "prefix", first: true, predicate: TextAreaPrefixComponent, descendants: true }, { propertyName: "suffix", first: true, predicate: TextAreaSuffixComponent, descendants: true }], exportAs: ["kendoTextArea"], usesInheritance: true, usesOnChanges: true, ngImport: i0, template: `
<ng-container
kendoInputSharedEvents
[hostElement]="hostElement"
[(isFocused)]="isFocused"
(handleBlur)="handleBlur()"
(onFocus)="handleFocus()"
>
<ng-content select="kendo-textarea-prefix"></ng-content>
<kendo-input-separator
*ngIf="prefix && showPrefixSeparator"
[orientation]="separatorOrientation"
></kendo-input-separator>
<textarea #input
class="k-input-inner !k-overflow-auto"
[attr.aria-multiline]="true"
[attr.aria-disabled]="disabled ? true : undefined"
[attr.aria-readonly]="readonly ? true : undefined"
[attr.aria-invalid]="isControlInvalid"
[id]="focusableId"
[attr.required]="isControlRequired ? '' : null"
[ngClass]="resizableClass"
[value]="value"
[attr.placeholder]="placeholder"
[disabled]="disabled"
[readonly]="readonly"
[attr.rows]="rows"
[attr.cols]="cols"
[attr.tabindex]="tabIndex"
[attr.title]="title"
[attr.maxlength]="maxlength"
[attr.aria-invalid]="isControlInvalid"
[kendoEventsOutsideAngular]="{
focus: handleInputFocus,
blur: handleInputBlur,
input: handleInput}"
></textarea>
<kendo-input-separator
*ngIf="suffix && showSuffixSeparator"
[orientation]="separatorOrientation"
></kendo-input-separator>
<ng-content select="kendo-textarea-suffix"></ng-content>
</ng-container>
`, isInline: true, dependencies: [{ kind: "directive", type: SharedInputEventsDirective, selector: "[kendoInputSharedEvents]", inputs: ["hostElement", "clearButtonClicked", "isFocused"], outputs: ["isFocusedChange", "onFocus", "handleBlur"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: InputSeparatorComponent, selector: "kendo-input-separator, kendo-textbox-separator", inputs: ["orientation"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: EventsOutsideAngularDirective, selector: "[kendoEventsOutsideAngular]", inputs: ["kendoEventsOutsideAngular", "scope"] }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: TextAreaComponent, decorators: [{
type: Component,
args: [{
exportAs: 'kendoTextArea',
providers: [
LocalizationService,
{ provide: L10N_PREFIX, useValue: 'kendo.textarea' },
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TextAreaComponent),
multi: true
},
{ provide: KendoInput, useExisting: forwardRef(() => TextAreaComponent) }
],
selector: 'kendo-textarea',
template: `
<ng-container
kendoInputSharedEvents
[hostElement]="hostElement"
[(isFocused)]="isFocused"
(handleBlur)="handleBlur()"
(onFocus)="handleFocus()"
>
<ng-content select="kendo-textarea-prefix"></ng-content>
<kendo-input-separator
*ngIf="prefix && showPrefixSeparator"
[orientation]="separatorOrientation"
></kendo-input-separator>
<textarea #input
class="k-input-inner !k-overflow-auto"
[attr.aria-multiline]="true"
[attr.aria-disabled]="disabled ? true : undefined"
[attr.aria-readonly]="readonly ? true : undefined"
[attr.aria-invalid]="isControlInvalid"
[id]="focusableId"
[attr.required]="isControlRequired ? '' : null"
[ngClass]="resizableClass"
[value]="value"
[attr.placeholder]="placeholder"
[disabled]="disabled"
[readonly]="readonly"
[attr.rows]="rows"
[attr.cols]="cols"
[attr.tabindex]="tabIndex"
[attr.title]="title"
[attr.maxlength]="maxlength"
[attr.aria-invalid]="isControlInvalid"
[kendoEventsOutsideAngular]="{
focus: handleInputFocus,
blur: handleInputBlur,
input: handleInput}"
></textarea>
<kendo-input-separator
*ngIf="suffix && showSuffixSeparator"
[orientation]="separatorOrientation"
></kendo-input-separator>
<ng-content select="kendo-textarea-suffix"></ng-content>
</ng-container>
`,
standalone: true,
imports: [SharedInputEventsDirective, NgIf, InputSeparatorComponent, NgClass, EventsOutsideAngularDirective]
}]
}], ctorParameters: function () { return [{ type: i1.LocalizationService }, { type: i0.NgZone }, { type: i0.ChangeDetectorRef }, { type: i0.Renderer2 }, { type: i0.Injector }, { type: i0.ElementRef }]; }, propDecorators: { focusableId: [{
type: Input
}], hostClasses: [{
type: HostBinding,
args: ['class.k-textarea']
}, {
type: HostBinding,
args: ['class.k-input']
}], flowCol: [{
type: HostBinding,
args: ['class.\!k-flex-col']
}], flowRow: [{
type: HostBinding,
args: ['class.\!k-flex-row']
}], flow: [{
type: Input
}], inputAttributes: [{
type: Input
}], adornmentsOrientation: [{
type: Input
}], rows: [{
type: Input
}], cols: [{
type: Input
}], maxlength: [{
type: Input
}], tabindex: [{
type: Input
}], tabIndex: [{
type: Input
}], resizable: [{
type: Input
}], size: [{
type: Input
}], rounded: [{
type: Input
}], fillMode: [{
type: Input
}], showPrefixSeparator: [{
type: Input
}], showSuffixSeparator: [{
type: Input
}], onFocus: [{
type: Output,
args: ['focus']
}], onBlur: [{
type: Output,
args: ['blur']
}], valueChange: [{
type: Output
}], prefix: [{
type: ContentChild,
args: [TextAreaPrefixComponent]
}], suffix: [{
type: ContentChild,
args: [TextAreaSuffixComponent]
}] } });