@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
741 lines (740 loc) • 27.4 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, take } 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 { 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.
*
* Use this component to let users enter and edit multi-line text.
*
* @example
* ```html
* <kendo-textarea [(ngModel)]="value" [rows]="5" [cols]="30"></kendo-textarea>
* ```
*
* @remarks
* Supported children components are: {@link TextAreaPrefixComponent}, {@link TextAreaSuffixComponent}.
*/
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. Use this property to set the position of adornments relative to the text area.
*/
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. Some attributes are required for component functionality and 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. Use this property to set the position of adornments relative to each other.
*
*/
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;
}
/**
* Sets the visible height of the text area in lines.
*/
rows;
/**
* Sets the visible width of the text area in average character width.
*/
cols;
/**
* Sets the maximum number of characters allowed in the text area.
*/
maxlength;
/**
* @hidden
*/
maxResizableRows;
/**
* Sets the [`tabindex`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex) of the component.
* @default 0
*/
tabindex = 0;
/**
* @hidden
*/
set tabIndex(tabIndex) {
this.tabindex = tabIndex;
}
get tabIndex() {
return this.tabindex;
}
/**
* Sets the resize behavior of the TextArea.
*
*
* @default 'vertical'
*/
resizable = 'vertical';
/**
* Sets the size of the TextArea. Controls the padding of the text area element ([see example]({% slug appearance_textarea %}#toc-size)).
* @default 'medium'
*/
set size(size) {
const newSize = size ? size : DEFAULT_SIZE;
this.handleClasses(newSize, 'size');
this._size = newSize;
}
get size() {
return this._size;
}
/**
* Sets the border radius of the TextArea ([see example](slug:appearance_textarea#toc-roundness)).
* @default 'medium'
*/
set rounded(rounded) {
const newRounded = rounded ? rounded : DEFAULT_ROUNDED;
this.handleClasses(newRounded, 'rounded');
this._rounded = newRounded;
}
get rounded() {
return this._rounded;
}
/**
* Sets the background and border styles of the TextArea ([see example](slug:appearance_textarea#toc-fill-mode)).
* @default 'solid'
*/
set fillMode(fillMode) {
const newFillMode = fillMode ? fillMode : DEFAULT_FILL_MODE;
this.handleClasses(newFillMode, 'fillMode');
this._fillMode = newFillMode;
}
get fillMode() {
return this._fillMode;
}
/**
* Shows the prefix separator in the TextArea.
* The separator is rendered only if a prefix template is declared.
*
* @default false
*/
showPrefixSeparator = false;
/**
* Shows the suffix separator in the TextArea.
* The separator is rendered only if a suffix template is declared.
*
* @default false
*/
showSuffixSeparator = false;
/**
* Fires when the TextArea is focused.
*
* Use the `onFocus` property to subscribe to this event.
*/
onFocus = new EventEmitter();
/**
* Fires when the TextArea gets blurred.
*
* Use the `onBlur` property to subscribe to this event.
*/
onBlur = new EventEmitter();
/**
* Fires when the value changes or the TextArea is blurred ([see example](slug:events_textarea)).
*
* The event does not fire when the value changes programmatically or through form control binding.
*/
valueChange = new EventEmitter();
initialHeight;
maxResizableHeight;
resizeSubscription;
resizeObserver;
_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';
}
ngAfterContentInit() {
this.ngZone.onStable.pipe((take(1))).subscribe(() => {
this.prefix && (this.prefix.orientation = this.adornmentsOrientation);
this.suffix && (this.suffix.orientation = this.adornmentsOrientation);
});
}
ngAfterViewInit() {
this.ngZone.runOutsideAngular(() => {
this.handleFlow();
});
this.ngZone.onStable.pipe(take(1)).subscribe(() => {
if (this.prefix) {
this.prefix.flow = this.flow;
}
if (this.suffix) {
this.suffix.flow = this.flow;
}
});
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());
this.attachResizeObserver();
}
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.ngZone.onStable.pipe(take(1)).subscribe(() => {
this.initialHeight = element.offsetHeight;
if (this.maxResizableRows && this.rows && isDocumentAvailable()) {
const heightValue = parseFloat(getComputedStyle(element).getPropertyValue('height')) - 2 * parseFloat(getComputedStyle(element).getPropertyValue('padding'));
this.maxResizableHeight = this.initialHeight + (heightValue * (this.maxResizableRows - this.rows));
}
});
}
else if (this.resizable !== 'both') {
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;
}
ngOnDestroy() {
super.ngOnDestroy();
if (this.resizeSubscription) {
this.resizeSubscription.unsubscribe();
}
if (this.resizeObserver) {
this.resizeObserver.disconnect();
}
}
/**
* @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.
*/
focus() {
if (!this.input) {
return;
}
this.focusChangedProgrammatically = true;
this.isFocused = true;
this.input.nativeElement.focus();
this.focusChangedProgrammatically = false;
}
/**
* Blurs the TextArea.
*/
blur() {
this.focusChangedProgrammatically = true;
const isFocusedElement = this.hostElement.nativeElement.querySelector(':focus');
if (isFocusedElement) {
isFocusedElement.blur();
}
this.isFocused = false;
this.focusChangedProgrammatically = false;
}
attachResizeObserver() {
if (typeof ResizeObserver === 'undefined' || !this.hostElement?.nativeElement) {
return;
}
this.ngZone.runOutsideAngular(() => {
this.resizeObserver = new ResizeObserver(() => this.resize());
this.resizeObserver.observe(this.hostElement.nativeElement);
});
}
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;
if (scrollHeight > this.maxResizableHeight) {
this.renderer.setStyle(element, 'height', `${this.maxResizableHeight}px`);
return;
}
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;
});
}
updateValue(value) {
if (!areSame(this.value, value)) {
this.ngZone.run(() => {
this.value = value;
this.ngChange(value);
this.valueChange.emit(value);
this.changeDetector.markForCheck();
});
}
}
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: "18.2.14", 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: "17.0.0", version: "18.2.14", type: TextAreaComponent, isStandalone: true, selector: "kendo-textarea", inputs: { focusableId: "focusableId", flow: "flow", inputAttributes: "inputAttributes", adornmentsOrientation: "adornmentsOrientation", rows: "rows", cols: "cols", maxlength: "maxlength", maxResizableRows: "maxResizableRows", 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>
@if (prefix && showPrefixSeparator) {
<kendo-input-separator
[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>
@if (suffix && showSuffixSeparator) {
<kendo-input-separator
[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: "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: "18.2.14", 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>
@if (prefix && showPrefixSeparator) {
<kendo-input-separator
[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>
@if (suffix && showSuffixSeparator) {
<kendo-input-separator
[orientation]="separatorOrientation"
></kendo-input-separator>
}
<ng-content select="kendo-textarea-suffix"></ng-content>
</ng-container>
`,
standalone: true,
imports: [SharedInputEventsDirective, InputSeparatorComponent, NgClass, EventsOutsideAngularDirective]
}]
}], ctorParameters: () => [{ 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
}], maxResizableRows: [{
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]
}] } });