UNPKG

@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
/**----------------------------------------------------------------------------------------- * 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) &mdash;TextArea sections are placed from top to bottom. * * `horizontal`&mdash;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) &mdash;TextArea adornments are placed from left to right in `ltr`, and from right to left in `rtl` mode. * * `vertical`&mdash;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)&mdash;The TextArea component can be resized only vertically. * * `horizontal`&mdash;The TextArea component can be resized only horizontally. * * `both`&mdash;The TextArea component can be resized in both (horizontal and vertical) directions. * * `auto`&mdash;Specifies whether the TextArea component will adjust its height automatically, based on the content. * * `none`&mdash;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] }] } });