UNPKG

@progress/kendo-angular-progressbar

Version:

Kendo UI Angular component starter template

505 lines (504 loc) 22.6 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, 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] }] } });