@progress/kendo-angular-progressbar
Version:
Kendo UI Angular component starter template
505 lines (504 loc) • 22.6 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, HostBinding, Input, isDevMode, NgZone, Output, Renderer2, ViewChild } from '@angular/core';
import { hasObservers, isChanged, isDocumentAvailable, ResizeSensorComponent } from '@progress/kendo-angular-common';
import { L10N_PREFIX, LocalizationService } from '@progress/kendo-angular-l10n';
import { validatePackage } from '@progress/kendo-licensing';
import { Subscription } from 'rxjs';
import { take } from 'rxjs/operators';
import { hasElementSize, removeProgressBarStyles, setProgressBarStyles } from '../common/util';
import { packageMetadata } from '../package-metadata';
import { CircularProgressbarCenterTemplateDirective } from './center-template.directive';
import { NgIf, NgTemplateOutlet } from '@angular/common';
import { LocalizedProgressBarMessagesDirective } from '../common/localization/localized-messages.directive';
import * as i0 from "@angular/core";
import * as i1 from "@progress/kendo-angular-l10n";
const DEFAULT_SURFACE_SIZE = 200;
/**
* Represents the [Kendo UI Circular ProgressBar component for Angular]({% slug overview_circularprogressbar %}).
*
* @example
* ```ts-preview
* _@Component({
* selector: 'my-app',
* template: `
* <kendo-circularprogressbar [value]="value"></kendo-circularprogressbar>
* `
* })
* class AppComponent {
* public value: number = 50;
* }
* ```
*/
export class CircularProgressBarComponent {
renderer;
cdr;
localization;
element;
zone;
hostClasses = true;
get ariaMinAttribute() {
return String(this.min);
}
get ariaMaxAttribute() {
return String(this.max);
}
get ariaValueAttribute() {
return this.indeterminate ? undefined : String(this.value);
}
roleAttribute = 'progressbar';
/**
* Sets the default value of the Circular Progressbar between `min` and `max`.
*
* @default 0
*/
set value(value) {
if (value > this.max) {
this.handleErrors('value > max');
}
if (value < this.min) {
this.handleErrors('value < min');
}
this.previousValue = this.value;
this._value = value;
}
get value() {
return this._value;
}
/**
* The maximum value which the Circular Progressbar can accept.
*
* @default 100
*/
set max(max) {
if (max < this.min) {
this.handleErrors('max < min');
}
this._max = max;
}
get max() {
return this._max;
}
/**
* The minimum value which the Circular Progressbar can accept.
*
* @default 0
*/
set min(min) {
if (min > this.max) {
this.handleErrors('max < min');
}
this._min = min;
}
get min() {
return this._min;
}
/**
* Indicates whether an animation will be played on value changes.
*
* @default false
*/
animation = false;
/**
* The opacity of the value arc.
* @default 1
*/
opacity = 1;
/**
* Puts the Circular ProgressBar in indeterminate state.
* @default false
*/
set indeterminate(indeterminate) {
this._indeterminate = indeterminate;
}
get indeterminate() {
return this._indeterminate;
}
/**
* Configures the pointer color. Could be set to a single color string or customized per progress stages.
*/
progressColor;
/**
* Fires when the animation which indicates the latest value change is completed.
*/
animationEnd = new EventEmitter();
progress;
scale;
labelElement;
surface;
centerTemplate;
centerTemplateContext = {};
_indeterminate = false;
_max = 100;
_min = 0;
_value = 0;
previousValue = 0;
internalValue = 0;
rtl;
subscriptions = new Subscription();
constructor(renderer, cdr, localization, element, zone) {
this.renderer = renderer;
this.cdr = cdr;
this.localization = localization;
this.element = element;
this.zone = zone;
validatePackage(packageMetadata);
this.subscriptions.add(this.localization.changes.subscribe(this.rtlChange.bind(this)));
}
ngAfterViewInit() {
if (!isDocumentAvailable()) {
return;
}
const elem = this.element.nativeElement;
const ariaLabel = this.localization.get('progressBarLabel');
this.renderer.setAttribute(elem, 'aria-label', ariaLabel);
this.initProgressArc();
}
ngOnChanges(changes) {
const skipFirstChange = true;
if (isChanged('value', changes, skipFirstChange) && this.progress) {
if (this.animation) {
this.progressbarAnimation();
}
else {
const value = this.value - this.min;
this.internalValue = changes['value'].currentValue;
this.calculateProgress(value);
}
}
if (changes['opacity'] && this.progress) {
setProgressBarStyles([{ method: 'setAttribute', el: this.progress.nativeElement, attr: 'opacity', attrValue: this.opacity.toString() }], this.renderer);
}
if (changes['indeterminate'] && !changes['indeterminate'].firstChange) {
this.indeterminateState();
}
}
ngOnDestroy() {
this.subscriptions.unsubscribe();
}
/**
* @hidden
*/
onResize() {
this.setStyles();
const value = this.animation ? this.internalValue : this.value;
this.calculateProgress(value);
this.updateCenterTemplate(value);
}
initProgressArc() {
this.setStyles();
if (this.indeterminate) {
this.indeterminateState();
}
else {
if (!this.animation) {
const value = this.value - this.min;
this.calculateProgress(value);
}
else {
this.progressbarAnimation();
}
}
}
calculateProgress(value) {
if (this.progressColor) {
this.updateProgressColor(value);
}
// needed when we have *ngIf inside the template to render different content depending on some condition
this.zone.onStable.pipe(take(1)).subscribe(() => {
this.updateCenterTemplate(value + this.min);
});
const progressArc = this.progress.nativeElement;
const radius = this.progress.nativeElement.r.baseVal.value;
const circumference = Math.PI * (radius * 2);
const dir = this.rtl ? circumference * -1 : circumference;
const strokeDashOffest = circumference - dir * (value / (this.max - this.min));
const progressCalculations = [
{ method: 'setStyle', el: progressArc, attr: 'strokeDasharray', attrValue: circumference.toString() },
{ method: 'setStyle', el: progressArc, attr: 'strokeDashoffset', attrValue: strokeDashOffest.toString() }
];
setProgressBarStyles(progressCalculations, this.renderer);
}
progressbarAnimation() {
const forwardProgress = {
isOngoing: this.internalValue > this.value - this.min,
isPositive: this.value >= this.previousValue
};
const backwardProgress = {
isOngoing: this.internalValue < this.value - this.min,
isNegative: this.value <= this.previousValue
};
if (forwardProgress.isOngoing && forwardProgress.isPositive ||
backwardProgress.isOngoing && backwardProgress.isNegative) {
return;
}
this.calculateProgress(this.internalValue);
const from = this.internalValue;
if (hasObservers(this.animationEnd)) {
this.animationEnd.emit({
from: from,
to: this.internalValue
});
}
// eslint-disable-next-line no-unused-expressions
forwardProgress.isPositive ? this.internalValue += 1 : this.internalValue -= 1;
requestAnimationFrame(this.progressbarAnimation.bind(this));
}
setStyles() {
const progressArc = this.progress.nativeElement;
const scale = this.scale.nativeElement;
const surface = this.surface.nativeElement;
const element = this.element.nativeElement;
let elWidth = element.getBoundingClientRect().width;
if (!hasElementSize(element)) {
const surfaceSize = [
{ method: 'setStyle', el: surface, attr: 'width', attrValue: `${DEFAULT_SURFACE_SIZE}px` },
{ method: 'setStyle', el: surface, attr: 'height', attrValue: `${DEFAULT_SURFACE_SIZE}px` }
];
elWidth = DEFAULT_SURFACE_SIZE;
setProgressBarStyles(surfaceSize, this.renderer);
}
const attributesArray = [
{ method: 'setAttribute', el: progressArc, attr: 'r', attrValue: String((elWidth / 2) - 10) },
{ method: 'setAttribute', el: progressArc, attr: 'cx', attrValue: String((elWidth / 2)) },
{ method: 'setAttribute', el: progressArc, attr: 'cy', attrValue: String((elWidth / 2)) },
{ method: 'setAttribute', el: progressArc, attr: 'opacity', attrValue: String(this.opacity) },
{ method: 'setAttribute', el: scale, attr: 'r', attrValue: String((elWidth / 2) - 10) },
{ method: 'setAttribute', el: scale, attr: 'cx', attrValue: String(elWidth / 2) },
{ method: 'setAttribute', el: scale, attr: 'cy', attrValue: String(elWidth / 2) }
];
setProgressBarStyles(attributesArray, this.renderer);
}
indeterminateState() {
const progressArc = this.progress.nativeElement;
if (this.indeterminate) {
// the indeterminate state wont work as the `k-circular-progressbar-arc` has a transform: rotate(-90deg) which is
// interfering with the svg animation as the animateTransform brings its own transform: rotate()
// This will be like this until the themes release a new version, bringing a new class `k-circular-progressbar-indeterminate-arc`
// containing only the necassery CSS styles and we will switch between them when the state of the progressbar is switched.
this.calculateProgress(this.value - this.min);
const rotate = this.rtl ? { from: 360, to: 0 } : { from: 0, to: 360 };
let color;
if (!this.progressColor) {
color = getComputedStyle(progressArc).stroke;
}
const indeterminateStyles = [
{ method: 'setStyle', el: progressArc, attr: 'transform-origin', attrValue: 'center' },
{ method: 'setStyle', el: progressArc, attr: 'fill', attrValue: 'none' },
{ method: 'setStyle', el: progressArc, attr: 'stroke-linecap', attrValue: 'round' },
{ method: 'setStyle', el: progressArc, attr: 'stroke', attrValue: color ? color : this.currentColor }
];
setProgressBarStyles(indeterminateStyles, this.renderer);
this.renderer.removeClass(progressArc, 'k-circular-progressbar-arc');
progressArc.innerHTML = `<animateTransform attributeName="transform" type="rotate" from="${rotate.from} 0 0" to="${rotate.to} 0 0" dur="1s" repeatCount="indefinite" />`;
}
else {
this.renderer.addClass(progressArc, 'k-circular-progressbar-arc');
const removeIndeterminateStyles = [
{ method: 'removeStyle', el: progressArc, attr: 'transform-origin' },
{ method: 'removeStyle', el: progressArc, attr: 'fill' },
{ method: 'removeStyle', el: progressArc, attr: 'stroke-linecap' }
];
removeProgressBarStyles(removeIndeterminateStyles, this.renderer);
progressArc.innerHTML = '';
if (this.animation) {
this.progressbarAnimation();
}
}
}
updateCenterTemplate(value) {
if (!this.centerTemplate) {
return;
}
this.centerTemplateContext.value = value;
this.centerTemplateContext.color = this.currentColor;
this.cdr.detectChanges();
this.positionLabel();
}
positionLabel() {
const labelEl = this.labelElement.nativeElement;
const element = this.element.nativeElement;
const surface = this.surface.nativeElement;
let elWidth;
let elHeight;
if (!hasElementSize(element)) {
const surfaceSize = surface.getBoundingClientRect();
elWidth = surfaceSize.width;
elHeight = surfaceSize.height;
}
else {
const elementSize = element.getBoundingClientRect();
elWidth = elementSize.width;
elHeight = elementSize.height;
}
const left = (elWidth / 2) - (labelEl.offsetWidth / 2);
const top = (elHeight / 2) - (labelEl.offsetHeight / 2);
const labelCalculations = [
{ method: 'setStyle', el: labelEl, attr: 'left', attrValue: `${left}px` },
{ method: 'setStyle', el: labelEl, attr: 'top', attrValue: `${top}px` }
];
setProgressBarStyles(labelCalculations, this.renderer);
}
get currentColor() {
const currentColor = this.progress.nativeElement.style.stroke;
return currentColor;
}
updateProgressColor(value) {
const progressArc = this.progress.nativeElement;
if (typeof this.progressColor === 'string') {
this.renderer.setStyle(progressArc, 'stroke', this.progressColor);
}
else {
for (let i = 0; i < this.progressColor.length; i++) {
const from = this.progressColor[i].from;
const to = this.progressColor[i].to;
if (value >= from && value <= to || (!from && value <= to)) {
this.renderer.setStyle(progressArc, 'stroke', this.progressColor[i].color);
break;
}
if (!to && value >= from) {
this.renderer.setStyle(progressArc, 'stroke', this.progressColor[i].color);
}
}
}
}
handleErrors(type) {
if (isDevMode()) {
switch (type) {
case 'value > max':
throw new Error('The value of the CircularProgressbar cannot exceed the max value');
case 'value < min':
throw new Error('The value of the CircularProgressbar cannot be lower than the min value');
case 'max < min':
throw new Error('The min value cannot be higher than the max value');
default:
}
}
}
setDirection() {
this.rtl = this.localization.rtl;
if (this.element) {
this.renderer.setAttribute(this.element.nativeElement, 'dir', this.rtl ? 'rtl' : 'ltr');
}
if (this.labelElement) {
this.renderer.setAttribute(this.labelElement.nativeElement, 'dir', this.rtl ? 'rtl' : 'ltr');
}
}
rtlChange() {
if (this.element && this.rtl !== this.localization.rtl) {
this.setDirection();
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: CircularProgressBarComponent, deps: [{ token: i0.Renderer2 }, { token: i0.ChangeDetectorRef }, { token: i1.LocalizationService }, { token: i0.ElementRef }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: CircularProgressBarComponent, isStandalone: true, selector: "kendo-circularprogressbar", inputs: { value: "value", max: "max", min: "min", animation: "animation", opacity: "opacity", indeterminate: "indeterminate", progressColor: "progressColor" }, outputs: { animationEnd: "animationEnd" }, host: { properties: { "class.k-circular-progressbar": "this.hostClasses", "attr.aria-valuemin": "this.ariaMinAttribute", "attr.aria-valuemax": "this.ariaMaxAttribute", "attr.aria-valuenow": "this.ariaValueAttribute", "attr.role": "this.roleAttribute" } }, providers: [
LocalizationService,
{
provide: L10N_PREFIX,
useValue: 'kendo.circularprogressbar'
}
], queries: [{ propertyName: "centerTemplate", first: true, predicate: CircularProgressbarCenterTemplateDirective, descendants: true }], viewQueries: [{ propertyName: "progress", first: true, predicate: ["progress"], descendants: true }, { propertyName: "scale", first: true, predicate: ["scale"], descendants: true }, { propertyName: "labelElement", first: true, predicate: ["label"], descendants: true }, { propertyName: "surface", first: true, predicate: ["surface"], descendants: true }], exportAs: ["kendoCircularProgressBar"], usesOnChanges: true, ngImport: i0, template: `
<ng-container kendoProgressBarLocalizedMessages
i18n-progressBarLabel="kendo.circularprogressbar.progressBarLabel|The aria-label attribute for the Circular ProgressBar component."
progressBarLabel="Circular progressbar"
>
</ng-container>
<div #surface class="k-circular-progressbar-surface">
<div>
<svg #svg>
<g>
<circle class="k-circular-progressbar-scale" #scale stroke-width="9.5"></circle>
<circle class="k-circular-progressbar-arc" #progress stroke-width="9.5"></circle>
</g>
</svg>
<div class="k-circular-progressbar-label" *ngIf="centerTemplate" #label>
<ng-template [ngTemplateOutlet]="centerTemplate.templateRef" [ngTemplateOutletContext]="centerTemplateContext"></ng-template>
</div>
</div>
</div>
<kendo-resize-sensor (resize)="onResize()"></kendo-resize-sensor>
`, isInline: true, dependencies: [{ kind: "directive", type: LocalizedProgressBarMessagesDirective, selector: "[kendoProgressBarLocalizedMessages]" }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: ResizeSensorComponent, selector: "kendo-resize-sensor", inputs: ["rateLimit"], outputs: ["resize"] }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: CircularProgressBarComponent, decorators: [{
type: Component,
args: [{
exportAs: 'kendoCircularProgressBar',
selector: 'kendo-circularprogressbar',
template: `
<ng-container kendoProgressBarLocalizedMessages
i18n-progressBarLabel="kendo.circularprogressbar.progressBarLabel|The aria-label attribute for the Circular ProgressBar component."
progressBarLabel="Circular progressbar"
>
</ng-container>
<div #surface class="k-circular-progressbar-surface">
<div>
<svg #svg>
<g>
<circle class="k-circular-progressbar-scale" #scale stroke-width="9.5"></circle>
<circle class="k-circular-progressbar-arc" #progress stroke-width="9.5"></circle>
</g>
</svg>
<div class="k-circular-progressbar-label" *ngIf="centerTemplate" #label>
<ng-template [ngTemplateOutlet]="centerTemplate.templateRef" [ngTemplateOutletContext]="centerTemplateContext"></ng-template>
</div>
</div>
</div>
<kendo-resize-sensor (resize)="onResize()"></kendo-resize-sensor>
`, providers: [
LocalizationService,
{
provide: L10N_PREFIX,
useValue: 'kendo.circularprogressbar'
}
],
standalone: true,
imports: [LocalizedProgressBarMessagesDirective, NgIf, NgTemplateOutlet, ResizeSensorComponent]
}]
}], ctorParameters: function () { return [{ type: i0.Renderer2 }, { type: i0.ChangeDetectorRef }, { type: i1.LocalizationService }, { type: i0.ElementRef }, { type: i0.NgZone }]; }, propDecorators: { hostClasses: [{
type: HostBinding,
args: ['class.k-circular-progressbar']
}], ariaMinAttribute: [{
type: HostBinding,
args: ['attr.aria-valuemin']
}], ariaMaxAttribute: [{
type: HostBinding,
args: ['attr.aria-valuemax']
}], ariaValueAttribute: [{
type: HostBinding,
args: ['attr.aria-valuenow']
}], roleAttribute: [{
type: HostBinding,
args: ['attr.role']
}], value: [{
type: Input
}], max: [{
type: Input
}], min: [{
type: Input
}], animation: [{
type: Input
}], opacity: [{
type: Input
}], indeterminate: [{
type: Input
}], progressColor: [{
type: Input
}], animationEnd: [{
type: Output
}], progress: [{
type: ViewChild,
args: ['progress']
}], scale: [{
type: ViewChild,
args: ['scale']
}], labelElement: [{
type: ViewChild,
args: ["label"]
}], surface: [{
type: ViewChild,
args: ["surface"]
}], centerTemplate: [{
type: ContentChild,
args: [CircularProgressbarCenterTemplateDirective]
}] } });