@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
JavaScript
/**-----------------------------------------------------------------------------------------
* 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']
}] } });