UNPKG

ngx-countdown

Version:

Simple, easy and performance countdown for angular

286 lines (279 loc) 10.7 kB
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: ` @if (render()) { <ng-container *ngTemplateOutlet="render(); context: { $implicit: i() }" /> } @else { <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: ` @if (render()) { <ng-container *ngTemplateOutlet="render(); context: { $implicit: i() }" /> } @else { <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