ngx-countdown
Version:
Simple, easy and performance countdown for angular
286 lines (279 loc) • 10.7 kB
JavaScript
import * as i0 from '@angular/core';
import { Injectable, InjectionToken, makeEnvironmentProviders, inject, LOCALE_ID, signal, input, output, afterNextRender, effect, ChangeDetectionStrategy, ViewEncapsulation, Component } from '@angular/core';
import { formatDate, NgTemplateOutlet } from '@angular/common';
var CountdownStatus;
(function (CountdownStatus) {
CountdownStatus[CountdownStatus["ing"] = 0] = "ing";
CountdownStatus[CountdownStatus["pause"] = 1] = "pause";
CountdownStatus[CountdownStatus["stop"] = 2] = "stop";
CountdownStatus[CountdownStatus["done"] = 3] = "done";
})(CountdownStatus || (CountdownStatus = {}));
class CountdownTimer {
fns = [];
commands = [];
nextTime = 0;
ing = false;
start() {
if (this.ing === true) {
return;
}
this.ing = true;
this.nextTime = +new Date();
this.process();
}
process() {
while (this.commands.length) {
this.commands.shift()();
}
let diff = +new Date() - this.nextTime;
const count = 1 + Math.floor(diff / 100);
diff = 100 - (diff % 100);
this.nextTime += 100 * count;
for (let i = 0, len = this.fns.length; i < len; i += 2) {
let frequency = this.fns[i + 1];
// 100/s
if (0 === frequency) {
this.fns[i](count);
// 1000/s
}
else {
// 先把末位至0,再每次加2
frequency += 2 * count - 1;
const step = Math.floor(frequency / 20);
if (step > 0) {
this.fns[i](step);
}
// 把末位还原成1
this.fns[i + 1] = (frequency % 20) + 1;
}
}
if (!this.ing) {
return;
}
setTimeout(() => this.process(), diff);
}
add(fn, frequency) {
this.commands.push(() => {
this.fns.push(fn);
this.fns.push(frequency === 1000 ? 1 : 0);
this.ing = true;
});
return this;
}
remove(fn) {
this.commands.push(() => {
const i = this.fns.indexOf(fn);
if (i !== -1) {
this.fns.splice(i, 2);
}
this.ing = this.fns.length > 0;
});
return this;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.1", ngImport: i0, type: CountdownTimer, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.1", ngImport: i0, type: CountdownTimer });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.1", ngImport: i0, type: CountdownTimer, decorators: [{
type: Injectable
}] });
const COUNTDOWN_CONFIG = new InjectionToken('COUNTDOWN_CONFIG');
function provideCountdown(config) {
return makeEnvironmentProviders([{ provide: COUNTDOWN_CONFIG, useValue: config }]);
}
class CountdownComponent {
locale = inject(LOCALE_ID);
timer = inject(CountdownTimer);
defCog = inject(COUNTDOWN_CONFIG, { optional: true });
frequency = 1000;
_notify = {};
status = CountdownStatus.ing;
isDestroy = false;
_config;
left = 0;
i = signal({}, { ...(ngDevMode ? { debugName: "i" } : {}) });
config = input.required({ ...(ngDevMode ? { debugName: "config" } : {}), transform: (i) => {
if (i.notify != null && !Array.isArray(i.notify) && i.notify > 0) {
i.notify = [i.notify];
}
return i;
} });
render = input(undefined, { ...(ngDevMode ? { debugName: "render" } : {}) });
event = output();
constructor() {
afterNextRender(() => {
this.init();
if (!this._config?.demand) {
this.begin();
}
});
let cfgFirst = true;
effect(() => {
this.config();
if (cfgFirst) {
cfgFirst = false;
return;
}
this.restart();
});
}
/**
* Start countdown, you must manually call when `demand: false`
*/
begin() {
this.status = CountdownStatus.ing;
this.callEvent('start');
}
/**
* Restart countdown
*/
restart() {
if (this.status !== CountdownStatus.stop) {
this.destroy();
}
this.init();
this.callEvent('restart');
}
/**
* Stop countdown, must call `restart` when stopped, it's different from pause, unable to recover
*/
stop() {
if (this.status === CountdownStatus.stop) {
return;
}
this.status = CountdownStatus.stop;
this.destroy();
this.callEvent('stop');
}
/**
* Pause countdown, you can use `resume` to recover again
*/
pause() {
if (this.status === CountdownStatus.stop || this.status === CountdownStatus.pause) {
return;
}
this.status = CountdownStatus.pause;
this.callEvent('pause');
}
/**
* Resume countdown
*/
resume() {
if (this.status === CountdownStatus.stop || this.status !== CountdownStatus.pause) {
return;
}
this.status = CountdownStatus.ing;
this.callEvent('resume');
}
callEvent(action) {
this.event.emit({ action, left: this.left, status: this.status, text: this.i().text });
}
init() {
const config = {
demand: false,
leftTime: 0,
format: 'HH:mm:ss',
timezone: '+0000',
formatDate: ({ date, formatStr, timezone }) => {
return formatDate(new Date(date), formatStr, this.locale, timezone || '+0000');
},
...this.defCog,
...this.config(),
};
this._config = config;
const frq = (this.frequency = ~config.format.indexOf('S') ? 100 : 1000);
this.status = config.demand ? CountdownStatus.pause : CountdownStatus.ing;
this.getLeft();
// bind reflow to me
const _reflow = this.reflow;
this.reflow = (count = 0, force = false) => _reflow.apply(this, [count, force]);
if (Array.isArray(config.notify)) {
config.notify.forEach((time) => {
if (time < 1) {
throw new Error(`The notify config must be a positive integer.`);
}
time = time * 1000;
time = time - (time % frq);
this._notify[time] = true;
});
}
this.timer.add(this.reflow, frq).start();
this.reflow(0, true);
}
destroy() {
this.timer.remove(this.reflow);
return this;
}
/**
* 更新时钟
*/
reflow(count = 0, force = false) {
if (this.isDestroy || this._config == null) {
return;
}
const { status, _notify } = this;
if (!force && status !== CountdownStatus.ing) {
return;
}
let value = (this.left = this.left - this.frequency * count);
if (value < 1) {
value = 0;
}
const { formatDate, format, timezone, prettyText, notify } = this._config;
const item = {
value,
text: formatDate({ date: value, formatStr: format, timezone: timezone }),
};
if (typeof prettyText === 'function') {
item.text = prettyText(item.text);
}
this.i.set(item);
if (notify === 0 || _notify[value]) {
this.callEvent('notify');
}
if (value === 0) {
this.status = CountdownStatus.done;
this.destroy();
this.callEvent('done');
}
}
/**
* 获取倒计时剩余帧数
*/
getLeft() {
const { frequency } = this;
const { leftTime, stopTime } = this._config;
let left = leftTime * 1000;
const end = stopTime;
if (!left && end) {
left = end - new Date().getTime();
}
this.left = left - (left % frequency);
}
ngOnDestroy() {
this.isDestroy = true;
this.destroy();
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.1", ngImport: i0, type: CountdownComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.1", type: CountdownComponent, isStandalone: true, selector: "countdown", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: true, transformFunction: null }, render: { classPropertyName: "render", publicName: "render", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { event: "event" }, host: { classAttribute: "count-down" }, providers: [CountdownTimer], ngImport: i0, template: `
(render()) {
<ng-container *ngTemplateOutlet="render(); context: { $implicit: i() }" />
} {
<span [innerHTML]="i().text"></span>
}
`, isInline: true, styles: [".count-down{font-variant-numeric:tabular-nums}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.1", ngImport: i0, type: CountdownComponent, decorators: [{
type: Component,
args: [{ selector: 'countdown', template: `
(render()) {
<ng-container *ngTemplateOutlet="render(); context: { $implicit: i() }" />
} {
<span [innerHTML]="i().text"></span>
}
`, host: { class: 'count-down' }, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgTemplateOutlet], providers: [CountdownTimer], styles: [".count-down{font-variant-numeric:tabular-nums}\n"] }]
}], ctorParameters: () => [], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: true }] }], render: [{ type: i0.Input, args: [{ isSignal: true, alias: "render", required: false }] }], event: [{ type: i0.Output, args: ["event"] }] } });
/**
* Generated bundle index. Do not edit.
*/
export { COUNTDOWN_CONFIG, CountdownComponent, CountdownStatus, CountdownTimer, provideCountdown };
//# sourceMappingURL=ngx-countdown.mjs.map