ng-circle-countdown
Version:
[](https://travis-ci.org/jlevot/ng-circle-countdown) [](https://badge.fury.io/js/ng-circle-countdown) [![npm
164 lines (157 loc) • 12.5 kB
JavaScript
import * as i0 from '@angular/core';
import { signal, computed, effect, Injectable, Pipe, output, input, inject, ContentChild, Component } from '@angular/core';
import { clearInterval, setInterval } from 'worker-timers';
import * as i1 from '@angular/common';
import { CommonModule } from '@angular/common';
const DEFAULT_COUNTER = {
isActive: false,
isCompleted: false,
speed: 1000,
remainingTime: 0
};
class CountdownService {
constructor() {
this.counter = signal(DEFAULT_COUNTER, ...(ngDevMode ? [{ debugName: "counter" }] : []));
this.isActive = computed(() => this.counter().isActive, ...(ngDevMode ? [{ debugName: "isActive" }] : []));
this.interval = 0;
this.start = () => this.counter.update((v) => ({ ...v, isActive: true }));
this.stop = () => this.counter.update((v) => ({ ...v, isActive: false }));
this.setRemainingTime = (remainingTime) => this.counter.update((c) => ({ ...c, remainingTime }));
this.resetCounter = (remainingTime) => this.counter.update((c) => ({ ...c, remainingTime, isCompleted: false }));
this.getCounter = () => {
return this.counter.asReadonly();
};
effect(() => this.tick(this.isActive()));
}
tick(isActive) {
clearInterval(this.interval);
if (isActive && this.counter().remainingTime !== 0) {
this.interval = setInterval(() => this.counter.set({
...this.counter(),
remainingTime: this.counter().remainingTime - this.counter().speed,
isCompleted: (this.counter().remainingTime - this.counter().speed) === 0
}), this.counter().speed);
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: CountdownService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: CountdownService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: CountdownService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}], ctorParameters: () => [] });
class SvgUtils {
static getRGB(color) {
const formattedColor = color.replace(/^#?([a-f\d])([a-f\d])([a-f\d])$/i, (m, r, g, b) => `#${r}${r}${g}${g}${b}${b}`);
const strippedColor = formattedColor.substring(1);
const colorChunks = strippedColor.match(/.{2}/g);
if (colorChunks)
return colorChunks.map(x => parseInt(x, 16));
return [];
}
static linearEase(time, start, goal, duration) {
if (duration === 0)
return start;
const currentTime = time / duration;
return start + goal * currentTime;
}
static getPathProps(size, strokeWidth, rotation) {
const halfSize = size / 2;
const halfStrokeWith = strokeWidth / 2;
const arcRadius = halfSize - halfStrokeWith;
const arcDiameter = 2 * arcRadius;
const rotationIndicator = rotation === 'clockwise' ? '1,0' : '0,1';
const length = Math.round(2 * Math.PI * arcRadius);
const path = `m ${halfSize},${halfStrokeWith} a ${arcRadius},${arcRadius} 0 ${rotationIndicator} 0,${arcDiameter} a ${arcRadius},${arcRadius} 0 ${rotationIndicator} 0,-${arcDiameter}`;
return { path, length };
}
}
class FormatTimePipe {
transform(time) {
return Math.round(time / 1000);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: FormatTimePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.1.2", ngImport: i0, type: FormatTimePipe, isStandalone: true, name: "format_time" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: FormatTimePipe, decorators: [{
type: Pipe,
args: [{
name: 'format_time',
standalone: true
}]
}] });
class CircleCountdownComponent {
constructor() {
this.countdownCompleted = output();
// Countdown duration
this.duration = input(0, ...(ngDevMode ? [{ debugName: "duration" }] : []));
// Primary color for the countdown displaying
this.color = input('#004777', ...(ngDevMode ? [{ debugName: "color" }] : []));
// Colors for the countdown displaying
this.colors = input([], ...(ngDevMode ? [{ debugName: "colors" }] : []));
// Time interval to dertermine when the countdown should change color
this.colorsTime = input([], ...(ngDevMode ? [{ debugName: "colorsTime" }] : []));
// Stroke width
this.strokeWidth = input(6, ...(ngDevMode ? [{ debugName: "strokeWidth" }] : []));
// Define the component size to match with your UI
this.size = input(180, ...(ngDevMode ? [{ debugName: "size" }] : []));
// Direction of the countdown rotation
this.rotation = input('clockwise', ...(ngDevMode ? [{ debugName: "rotation" }] : []));
this.countdownService = inject(CountdownService);
this.pathOptions = computed(() => SvgUtils.getPathProps(this.size(), this.strokeWidth(), this.rotation()), ...(ngDevMode ? [{ debugName: "pathOptions" }] : []));
this.countDown = this.countdownService.getCounter();
this.countDownData = computed(() => this.getCountDownData(this.countDown().remainingTime), ...(ngDevMode ? [{ debugName: "countDownData" }] : []));
this.isCompleted = computed(() => this.countDown().isCompleted, ...(ngDevMode ? [{ debugName: "isCompleted" }] : []));
effect(() => {
if (this.duration())
this.countdownService.setRemainingTime(this.duration());
if (this.isCompleted())
this.countdownCompleted.emit();
});
}
start() {
this.countdownService.start();
}
pause() {
this.countdownService.stop();
}
reload() {
this.countdownService.resetCounter(this.duration());
this.start();
}
getCountDownData(time) {
return {
currentColor: this.getStrokeColor(time),
offset: SvgUtils.linearEase(this.duration() - time, 0, this.pathOptions().length, this.duration())
};
}
getStrokeColor(time) {
if (this.colors().length === 0 && this.color())
return this.color();
const remainingTime = time / 1000;
const currentColorIndex = this.colorsTime()?.findIndex((time, i) => time >= remainingTime && remainingTime >= this.colorsTime()[i + 1]);
if (!this.colorsTime() || currentColorIndex === -1)
return this.colors()[0];
const currentTime = this.colorsTime()[currentColorIndex] - remainingTime;
const currentDuration = this.colorsTime()[currentColorIndex] - this.colorsTime()[currentColorIndex + 1];
const startColorRGB = SvgUtils.getRGB(this.colors()[currentColorIndex]);
const endColorRGB = SvgUtils.getRGB(this.colors()[(currentColorIndex + 1 <= this.colors().length - 1) ? currentColorIndex + 1 : this.colors().length - 1]);
return `rgb(${startColorRGB.map((color, index) => SvgUtils.linearEase(currentTime, color, endColorRGB[index] - color, currentDuration)).join(',')})`;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: CircleCountdownComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.1.2", type: CircleCountdownComponent, isStandalone: true, selector: "ng-circle-countdown", inputs: { duration: { classPropertyName: "duration", publicName: "duration", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, colors: { classPropertyName: "colors", publicName: "colors", isSignal: true, isRequired: false, transformFunction: null }, colorsTime: { classPropertyName: "colorsTime", publicName: "colorsTime", isSignal: true, isRequired: false, transformFunction: null }, strokeWidth: { classPropertyName: "strokeWidth", publicName: "strokeWidth", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, rotation: { classPropertyName: "rotation", publicName: "rotation", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { countdownCompleted: "countdownCompleted" }, queries: [{ propertyName: "counterTemplate", first: true, predicate: ["counter"], descendants: true }], ngImport: i0, template: "<div class=\"base-timer\" [style.width]=\"size() + 'px'\" [style.height]=\"size() + 'px'\">\r\n <svg [attr.viewBox]=\"'0 0 ' + size() + ' ' + size()\" [attr.width]=\"size()\" [attr.height]=\"size()\" xmlns=\"http://www.w3.org/2000/svg\">\r\n <path [attr.d]=\"pathOptions().path\" fill=\"none\" stroke='#d9d9d9' [attr.stroke-width]=\"strokeWidth()\"></path>\r\n\r\n <path [style.stroke]=\"countDownData().currentColor\"\r\n [attr.stroke-width]=\"strokeWidth()\"\r\n [attr.stroke-dashoffset]=\"countDownData().offset\"\r\n [attr.d]=\"pathOptions().path\"\r\n [attr.stroke-dasharray]=\"pathOptions().length\"\r\n stroke-linecap=\"round\"\r\n fill=\"none\">\r\n </path>\r\n </svg>\r\n <div class=\"time\" [style.color]=\"countDownData().currentColor\">\r\n <ng-container *ngTemplateOutlet=\"counterTemplate;\r\n context:{ $implicit: {value: countDown().remainingTime | format_time, color: countDownData().currentColor } }\">\r\n </ng-container>\r\n </div>\r\n</div>\r\n", styles: [":host .base-timer{position:relative}:host .base-timer .time{display:flex;justify-content:center;align-items:center;position:absolute;left:0;top:0;width:100%;height:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "pipe", type: FormatTimePipe, name: "format_time" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: CircleCountdownComponent, decorators: [{
type: Component,
args: [{ selector: 'ng-circle-countdown', imports: [CommonModule, FormatTimePipe], template: "<div class=\"base-timer\" [style.width]=\"size() + 'px'\" [style.height]=\"size() + 'px'\">\r\n <svg [attr.viewBox]=\"'0 0 ' + size() + ' ' + size()\" [attr.width]=\"size()\" [attr.height]=\"size()\" xmlns=\"http://www.w3.org/2000/svg\">\r\n <path [attr.d]=\"pathOptions().path\" fill=\"none\" stroke='#d9d9d9' [attr.stroke-width]=\"strokeWidth()\"></path>\r\n\r\n <path [style.stroke]=\"countDownData().currentColor\"\r\n [attr.stroke-width]=\"strokeWidth()\"\r\n [attr.stroke-dashoffset]=\"countDownData().offset\"\r\n [attr.d]=\"pathOptions().path\"\r\n [attr.stroke-dasharray]=\"pathOptions().length\"\r\n stroke-linecap=\"round\"\r\n fill=\"none\">\r\n </path>\r\n </svg>\r\n <div class=\"time\" [style.color]=\"countDownData().currentColor\">\r\n <ng-container *ngTemplateOutlet=\"counterTemplate;\r\n context:{ $implicit: {value: countDown().remainingTime | format_time, color: countDownData().currentColor } }\">\r\n </ng-container>\r\n </div>\r\n</div>\r\n", styles: [":host .base-timer{position:relative}:host .base-timer .time{display:flex;justify-content:center;align-items:center;position:absolute;left:0;top:0;width:100%;height:100%}\n"] }]
}], ctorParameters: () => [], propDecorators: { counterTemplate: [{
type: ContentChild,
args: ['counter']
}] } });
/**
* Generated bundle index. Do not edit.
*/
export { CircleCountdownComponent, CountdownService };
//# sourceMappingURL=ng-circle-countdown.mjs.map