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

755 lines (749 loc) 29.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 { ChangeDetectorRef, Component, ContentChild, ElementRef, EventEmitter, forwardRef, HostBinding, Input, NgZone, Output, Renderer2 } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { L10N_PREFIX, LocalizationService } from '@progress/kendo-angular-l10n'; import { isDocumentAvailable, KendoInput, Keys, normalizeKeys } from '@progress/kendo-angular-common'; import { validatePackage } from '@progress/kendo-licensing'; import { packageMetadata } from '../package-metadata'; import { starIcon, starOutlineIcon } from '@progress/kendo-svg-icons'; import { Subscription } from 'rxjs'; import { areSame } from '../common/utils'; import { RatingItemTemplateDirective } from './directives/rating-item.directive'; import { RatingHoveredItemTemplateDirective } from './directives/rating-hovered-item.directive'; import { RatingSelectedItemTemplateDirective } from './directives/rating-selected-item.directive'; import { NgClass, NgTemplateOutlet, NgStyle } from '@angular/common'; import { IconWrapperComponent } from '@progress/kendo-angular-icons'; import * as i0 from "@angular/core"; import * as i1 from "@progress/kendo-angular-l10n"; /** * Represents the Kendo UI Rating component for Angular. * Use this component to let users select a rating value. * * @example * ```html * <kendo-rating [itemsCount]="5" [(value)]="ratingValue"></kendo-rating> * ``` */ export class RatingComponent { element; renderer; localizationService; cdr; zone; itemTemplate; hoveredItemTemplate; selectedItemTemplate; /** * When `true`, disables the Rating ([see example]({% slug disabledstate_rating %})). * To disable the component in reactive forms, see [Forms Support](slug:formssupport_rating#toc-managing-the-rating-disabled-state-in-reactive-forms). * * @default false */ disabled = false; /** * When `true`, sets the Rating to read-only ([see example]({% slug readonly_rating %})). * * @default false */ readonly = false; /** * Sets the [`tabindex`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex) of the Rating. * * @default 0 */ tabindex = 0; /** * Sets the number of Rating items ([see example]({% slug itemscount_rating %})). * * @default 5 */ itemsCount = 5; /** * Sets the initial value of the Rating component. * Use either `ngModel` or the `value` binding, but not both at the same time. */ set value(value) { this._value = value; this.updateRatingItems(); } get value() { return this._value; } /** * Sets the selection mode of the Rating ([see example]({% slug selection_rating %})). * * @default 'continuous' */ set selection(selection) { this._selection = selection; this.updateRatingItems(); } get selection() { return this._selection; } /** * Sets the precision of the Rating ([see example]({% slug precision_rating %})). * * @default 'item' */ set precision(precision) { this._precision = precision; this.updateRatingItems(); } get precision() { return this._precision; } /** * Sets the label text for the Rating. The text renders in a `<span>` element ([see example]({% slug label_rating %})). */ label; /** * Sets a custom font icon for the Rating items ([see example]({% slug icon_rating %})). */ icon; /** * Sets a custom SVG icon for the selected or hovered state of the Rating items ([see example]({% slug icon_rating %})). */ svgIcon = starIcon; /** * Sets a custom SVG icon for the default state of the Rating items when not hovered or selected ([see example]({% slug icon_rating %})). */ svgIconOutline = starOutlineIcon; /** * Fires when the user selects a new value. */ valueChange = new EventEmitter(); hostClass = true; direction; get isControlInvalid() { return (this.control?.invalid)?.toString(); } valueMin = 0; get valueMax() { return this.itemsCount; } get valueNow() { return this.value; } ariaRole = 'slider'; /** * @hidden */ ratingItems = []; control; ngChange = (_) => { }; ngTouched = () => { }; rect; _value; _selection = 'continuous'; _precision = 'item'; subscriptions = new Subscription(); constructor(element, renderer, localizationService, cdr, zone) { this.element = element; this.renderer = renderer; this.localizationService = localizationService; this.cdr = cdr; this.zone = zone; validatePackage(packageMetadata); } ngOnInit() { this.subscriptions.add(this.localizationService .changes .subscribe(({ rtl }) => { this.direction = rtl ? 'rtl' : 'ltr'; })); this.subscriptions.add(this.renderer.listen(this.element.nativeElement, 'blur', () => this.ngTouched())); this.subscriptions.add(this.renderer.listen(this.element.nativeElement, 'keydown', event => this.onKeyDown(event))); this.createRatingItems(); } ngAfterViewInit() { const items = this.element.nativeElement.querySelectorAll('.k-rating-item'); this.zone.runOutsideAngular(() => { items.forEach((item, index) => this.subscriptions.add(this.renderer.listen(item, 'mousemove', (event) => this.onMouseMove(index, event)))); }); } ngOnDestroy() { this.subscriptions.unsubscribe(); } /** * Focuses the Rating component. */ focus() { if (isDocumentAvailable() && !this.disabled) { this.element.nativeElement.focus(); } } /** * Blurs the Rating component. */ blur() { if (isDocumentAvailable()) { this.element.nativeElement.blur(); } } /** * @hidden */ createRatingItems() { for (let i = 0; i < this.itemsCount; i++) { const item = { title: this.isHalf(i, this.value) ? String(i + 0.5) : String(i + 1), selected: this.isSelected(i, this.value), selectedIndicator: false, hovered: false, half: this.isHalf(i, this.value) }; this.ratingItems.push(item); } } /** * @hidden */ onMouseEnter(event) { this.rect = event.target.getBoundingClientRect(); } /** * @hidden */ onMouseMove(value, event) { const halfPrecision = this.precision === 'half'; const isFirstHalf = halfPrecision && this.isFirstHalf(this.rect, event.clientX); this.zone.run(() => this.ratingItems.forEach((item, index) => { item.title = (halfPrecision && value === index && isFirstHalf) ? String(index + 0.5) : String(index + 1); item.selected = item.hovered = this.isSelected(index, value + 1); item.selectedIndicator = this.isSelected(index, this.value); item.half = (halfPrecision && value === index) ? isFirstHalf : false; })); } /** * @hidden */ onMouseOut() { this.rect = null; this.updateRatingItems(); } /** * @hidden * Called when the status of the component changes to or from `disabled`. * Depending on the value, it enables or disables the appropriate DOM element. * * @param isDisabled */ setDisabledState(isDisabled) { this.disabled = isDisabled; this.cdr.markForCheck(); } /** * @hidden */ changeValue(index, event) { const rect = event.target.getBoundingClientRect(); const isFirstHalf = this.isFirstHalf(rect, event.clientX); const value = (this.precision === 'half' && isFirstHalf) ? index + 0.5 : index + 1; if (!areSame(this.value, value)) { this.value = value; this.ngChange(this.value); this.valueChange.emit(this.value); this.updateRatingItems(); this.cdr.markForCheck(); } } /** * @hidden */ updateRatingItems() { this.ratingItems.forEach((item, index) => { item.title = this.isHalf(index, this.value) ? String(index + 0.5) : String(index + 1); item.selected = this.isSelected(index, this.value); item.selectedIndicator = this.isSelected(index, this.value); item.hovered = false; item.half = this.isHalf(index, this.value); }); } /** * @hidden */ writeValue(value) { this.value = value; this.updateRatingItems(); this.cdr.markForCheck(); } /** * @hidden */ registerOnChange(fn) { this.ngChange = fn; } /** * @hidden */ registerOnTouched(fn) { this.ngTouched = fn; } isSelected(index, value) { return this.selection === 'single' ? index === Math.ceil(value - 1) : index <= Math.ceil(value - 1); } isHalf(index, value) { return (this.precision === 'half' && (value > index) && (value < index + 1)); } isFirstHalf(rect, clientX) { const elementPosition = rect.x + (rect.width / 2); return this.direction === 'ltr' ? clientX < elementPosition : clientX > elementPosition; } onKeyDown(event) { const decreaseValue = () => { if (this.value <= 0) { return; } this.value = (this.precision === 'half') ? this.value - 0.5 : this.value - 1; this.ngChange(this.value); this.valueChange.emit(this.value); this.updateRatingItems(); this.cdr.markForCheck(); }; const increaseValue = () => { if (this.value >= this.itemsCount) { return; } this.value = (this.precision === 'half') ? this.value + 0.5 : this.value + 1; this.ngChange(this.value); this.valueChange.emit(this.value); this.updateRatingItems(); this.cdr.markForCheck(); }; const setMinValue = () => { if (!areSame(this.value, this.valueMin)) { this.value = this.valueMin; this.ngChange(this.value); this.valueChange.emit(this.value); this.updateRatingItems(); this.cdr.markForCheck(); } }; const setMaxValue = () => { if (!areSame(this.value, this.valueMax)) { this.value = this.valueMax; this.ngChange(this.value); this.valueChange.emit(this.value); this.updateRatingItems(); this.cdr.markForCheck(); } }; const code = normalizeKeys(event); switch (code) { case Keys.ArrowDown: decreaseValue(); break; case Keys.ArrowLeft: if (this.direction === 'ltr') { decreaseValue(); } else { increaseValue(); } break; case Keys.ArrowUp: increaseValue(); break; case Keys.ArrowRight: if (this.direction === 'ltr') { increaseValue(); } else { decreaseValue(); } break; case Keys.Home: setMinValue(); break; case Keys.End: setMaxValue(); break; default: break; } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RatingComponent, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }, { token: i1.LocalizationService }, { token: i0.ChangeDetectorRef }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: RatingComponent, isStandalone: true, selector: "kendo-rating", inputs: { disabled: "disabled", readonly: "readonly", tabindex: "tabindex", itemsCount: "itemsCount", value: "value", selection: "selection", precision: "precision", label: "label", icon: "icon", svgIcon: "svgIcon", svgIconOutline: "svgIconOutline" }, outputs: { valueChange: "valueChange" }, host: { properties: { "attr.aria-disabled": "this.disabled", "class.k-disabled": "this.disabled", "attr.aria-readonly": "this.readonly", "class.k-readonly": "this.readonly", "attr.tabindex": "this.tabindex", "class.k-rating": "this.hostClass", "attr.dir": "this.direction", "attr.aria-invalid": "this.isControlInvalid", "attr.aria-valuemin": "this.valueMin", "attr.aria-valuemax": "this.valueMax", "attr.aria-valuenow": "this.valueNow", "attr.role": "this.ariaRole" } }, providers: [ LocalizationService, { provide: L10N_PREFIX, useValue: 'kendo.rating' }, { multi: true, provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => RatingComponent) /* eslint-disable-line*/ }, { provide: KendoInput, useExisting: forwardRef(() => RatingComponent) } ], queries: [{ propertyName: "itemTemplate", first: true, predicate: RatingItemTemplateDirective, descendants: true }, { propertyName: "hoveredItemTemplate", first: true, predicate: RatingHoveredItemTemplateDirective, descendants: true }, { propertyName: "selectedItemTemplate", first: true, predicate: RatingSelectedItemTemplateDirective, descendants: true }], exportAs: ["kendoRating"], ngImport: i0, template: ` <span class="k-rating-container"> @for (item of ratingItems; track item; let i = $index) { <span class="k-rating-item" [title]="item.title" [ngClass]="{ 'k-selected': item.selected || item.selectedIndicator, 'k-hover': item.hovered }" (mouseenter)="onMouseEnter($event)" (mouseout)="onMouseOut()" (click)="changeValue(i, $event)" > @if (!item.half) { @if (!itemTemplate) { @if (!icon) { <kendo-icon-wrapper size="xlarge" [name]="item.selected || item.hovered ? 'star' : 'star-outline'" [svgIcon]="item.selected || item.hovered ? svgIcon : svgIconOutline" > </kendo-icon-wrapper> } @if (icon) { <kendo-icon-wrapper size="xlarge" [name]="item.selected || item.hovered ? icon : icon + '-outline'" > </kendo-icon-wrapper> } } @if (itemTemplate && (!item.selected && !item.hovered)) { <ng-template [ngTemplateOutlet]="itemTemplate?.templateRef" [ngTemplateOutletContext]="{index: i}" > </ng-template> } @if (hoveredItemTemplate && item.hovered) { <ng-template [ngTemplateOutlet]="hoveredItemTemplate?.templateRef" [ngTemplateOutletContext]="{index: i}" > </ng-template> } @if (selectedItemTemplate && (item.selected && !item.hovered)) { <ng-template [ngTemplateOutlet]="selectedItemTemplate?.templateRef" [ngTemplateOutletContext]="{index: i}" > </ng-template> } } @if (item.half) { @if (!itemTemplate) { <span class="k-rating-precision-complement"> @if (!icon) { <kendo-icon-wrapper size="xlarge" [name]="'star-outline'" [svgIcon]="svgIconOutline" > </kendo-icon-wrapper> } @if (icon) { <kendo-icon-wrapper size="xlarge" [name]="icon + '-outline'" > </kendo-icon-wrapper> } </span> <span class="k-rating-precision-part" [ngStyle]="{'clipPath': direction === 'rtl' ? 'inset(0 0 0 50%)' : 'inset(0 50% 0 0)'}" > @if (!icon) { <kendo-icon-wrapper size="xlarge" [name]="'star'" [svgIcon]="svgIcon" > </kendo-icon-wrapper> } @if (icon) { <kendo-icon-wrapper size="xlarge" [name]="icon" > </kendo-icon-wrapper> } </span> } <span class="k-rating-precision-complement" > <ng-template [ngTemplateOutlet]="itemTemplate?.templateRef" [ngTemplateOutletContext]="{index: i}" > </ng-template> </span> @if (hoveredItemTemplate && item.hovered) { <span class="k-rating-precision-part" [ngStyle]="{'clipPath': direction === 'rtl' ? 'inset(0 0 0 50%)' : 'inset(0 50% 0 0)'}" > <ng-template [ngTemplateOutlet]="hoveredItemTemplate?.templateRef" [ngTemplateOutletContext]="{index: i}" > </ng-template> </span> } @if (selectedItemTemplate && (item.selected && !item.hovered)) { <span class="k-rating-precision-part" [ngStyle]="{'clipPath': direction === 'rtl' ? 'inset(0 0 0 50%)' : 'inset(0 50% 0 0)'}" > <ng-template [ngTemplateOutlet]="selectedItemTemplate?.templateRef" [ngTemplateOutletContext]="{index: i}" > </ng-template> </span> } <span [style.width.px]="24" [style.height.px]="24" [style.display]="'block'"></span> } </span> } </span> @if (label) { <span class="k-rating-label" >{{ label }}</span> } `, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: IconWrapperComponent, selector: "kendo-icon-wrapper", inputs: ["name", "svgIcon", "innerCssClass", "customFontClass", "size"], exportAs: ["kendoIconWrapper"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RatingComponent, decorators: [{ type: Component, args: [{ exportAs: 'kendoRating', providers: [ LocalizationService, { provide: L10N_PREFIX, useValue: 'kendo.rating' }, { multi: true, provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => RatingComponent) /* eslint-disable-line*/ }, { provide: KendoInput, useExisting: forwardRef(() => RatingComponent) } ], selector: 'kendo-rating', template: ` <span class="k-rating-container"> @for (item of ratingItems; track item; let i = $index) { <span class="k-rating-item" [title]="item.title" [ngClass]="{ 'k-selected': item.selected || item.selectedIndicator, 'k-hover': item.hovered }" (mouseenter)="onMouseEnter($event)" (mouseout)="onMouseOut()" (click)="changeValue(i, $event)" > @if (!item.half) { @if (!itemTemplate) { @if (!icon) { <kendo-icon-wrapper size="xlarge" [name]="item.selected || item.hovered ? 'star' : 'star-outline'" [svgIcon]="item.selected || item.hovered ? svgIcon : svgIconOutline" > </kendo-icon-wrapper> } @if (icon) { <kendo-icon-wrapper size="xlarge" [name]="item.selected || item.hovered ? icon : icon + '-outline'" > </kendo-icon-wrapper> } } @if (itemTemplate && (!item.selected && !item.hovered)) { <ng-template [ngTemplateOutlet]="itemTemplate?.templateRef" [ngTemplateOutletContext]="{index: i}" > </ng-template> } @if (hoveredItemTemplate && item.hovered) { <ng-template [ngTemplateOutlet]="hoveredItemTemplate?.templateRef" [ngTemplateOutletContext]="{index: i}" > </ng-template> } @if (selectedItemTemplate && (item.selected && !item.hovered)) { <ng-template [ngTemplateOutlet]="selectedItemTemplate?.templateRef" [ngTemplateOutletContext]="{index: i}" > </ng-template> } } @if (item.half) { @if (!itemTemplate) { <span class="k-rating-precision-complement"> @if (!icon) { <kendo-icon-wrapper size="xlarge" [name]="'star-outline'" [svgIcon]="svgIconOutline" > </kendo-icon-wrapper> } @if (icon) { <kendo-icon-wrapper size="xlarge" [name]="icon + '-outline'" > </kendo-icon-wrapper> } </span> <span class="k-rating-precision-part" [ngStyle]="{'clipPath': direction === 'rtl' ? 'inset(0 0 0 50%)' : 'inset(0 50% 0 0)'}" > @if (!icon) { <kendo-icon-wrapper size="xlarge" [name]="'star'" [svgIcon]="svgIcon" > </kendo-icon-wrapper> } @if (icon) { <kendo-icon-wrapper size="xlarge" [name]="icon" > </kendo-icon-wrapper> } </span> } <span class="k-rating-precision-complement" > <ng-template [ngTemplateOutlet]="itemTemplate?.templateRef" [ngTemplateOutletContext]="{index: i}" > </ng-template> </span> @if (hoveredItemTemplate && item.hovered) { <span class="k-rating-precision-part" [ngStyle]="{'clipPath': direction === 'rtl' ? 'inset(0 0 0 50%)' : 'inset(0 50% 0 0)'}" > <ng-template [ngTemplateOutlet]="hoveredItemTemplate?.templateRef" [ngTemplateOutletContext]="{index: i}" > </ng-template> </span> } @if (selectedItemTemplate && (item.selected && !item.hovered)) { <span class="k-rating-precision-part" [ngStyle]="{'clipPath': direction === 'rtl' ? 'inset(0 0 0 50%)' : 'inset(0 50% 0 0)'}" > <ng-template [ngTemplateOutlet]="selectedItemTemplate?.templateRef" [ngTemplateOutletContext]="{index: i}" > </ng-template> </span> } <span [style.width.px]="24" [style.height.px]="24" [style.display]="'block'"></span> } </span> } </span> @if (label) { <span class="k-rating-label" >{{ label }}</span> } `, standalone: true, imports: [NgClass, IconWrapperComponent, NgTemplateOutlet, NgStyle] }] }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.Renderer2 }, { type: i1.LocalizationService }, { type: i0.ChangeDetectorRef }, { type: i0.NgZone }], propDecorators: { itemTemplate: [{ type: ContentChild, args: [RatingItemTemplateDirective] }], hoveredItemTemplate: [{ type: ContentChild, args: [RatingHoveredItemTemplateDirective] }], selectedItemTemplate: [{ type: ContentChild, args: [RatingSelectedItemTemplateDirective] }], disabled: [{ type: Input }, { type: HostBinding, args: ['attr.aria-disabled'] }, { type: HostBinding, args: ['class.k-disabled'] }], readonly: [{ type: Input }, { type: HostBinding, args: ['attr.aria-readonly'] }, { type: HostBinding, args: ['class.k-readonly'] }], tabindex: [{ type: Input }, { type: HostBinding, args: ['attr.tabindex'] }], itemsCount: [{ type: Input }], value: [{ type: Input }], selection: [{ type: Input }], precision: [{ type: Input }], label: [{ type: Input }], icon: [{ type: Input }], svgIcon: [{ type: Input }], svgIconOutline: [{ type: Input }], valueChange: [{ type: Output }], hostClass: [{ type: HostBinding, args: ['class.k-rating'] }], direction: [{ type: HostBinding, args: ['attr.dir'] }], isControlInvalid: [{ type: HostBinding, args: ['attr.aria-invalid'] }], valueMin: [{ type: HostBinding, args: ['attr.aria-valuemin'] }], valueMax: [{ type: HostBinding, args: ['attr.aria-valuemax'] }], valueNow: [{ type: HostBinding, args: ['attr.aria-valuenow'] }], ariaRole: [{ type: HostBinding, args: ['attr.role'] }] } });