ngx-timeago
Version:
Live updating timestamps in Angular 6+.
401 lines (391 loc) • 17.1 kB
JavaScript
import * as i0 from '@angular/core';
import { Injectable, Directive, Optional, Input, Pipe, NgModule } from '@angular/core';
import { Subject, of, timer, empty } from 'rxjs';
import { expand, skip, filter } from 'rxjs/operators';
function isDefined(value) {
return typeof value !== 'undefined' && value !== null;
}
function coerceBooleanProperty(value) {
return value != null && `${value}` !== 'false';
}
function dateParser(date) {
const parsed = new Date(date);
if (!Number.isNaN(parsed.valueOf())) {
return parsed;
}
const parts = String(date).match(/\d+/g);
if (parts === null || parts.length <= 2) {
return parsed;
}
else {
const [firstP, secondP, ...restPs] = parts.map(x => parseInt(x, 10));
return new Date(Date.UTC(firstP, secondP - 1, ...restPs));
}
}
const MINUTE = 60;
const HOUR = MINUTE * 60;
const DAY = HOUR * 24;
const WEEK = DAY * 7;
const MONTH = DAY * 30;
const YEAR = DAY * 365;
;
/**
* To modify the text displayed, create a new instance of TimeagoIntl and
* include it in a custom provider
*/
class TimeagoIntl {
constructor() {
/**
* Stream that emits whenever the l10n strings are changed
* Use this to notify directives if the l10n strings have changed after initialization.
*/
this.changes = new Subject();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.1.1", ngImport: i0, type: TimeagoIntl, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.1.1", ngImport: i0, type: TimeagoIntl }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.1.1", ngImport: i0, type: TimeagoIntl, decorators: [{
type: Injectable
}] });
const defaultFormattter = function (then) {
const now = Date.now();
const seconds = Math.round(Math.abs(now - then) / 1000);
const suffix = then < now ? 'ago' : 'from now';
const [value, unit] = seconds < MINUTE
? [Math.round(seconds), 'second']
: seconds < HOUR
? [Math.round(seconds / MINUTE), 'minute']
: seconds < DAY
? [Math.round(seconds / HOUR), 'hour']
: seconds < WEEK
? [Math.round(seconds / DAY), 'day']
: seconds < MONTH
? [Math.round(seconds / WEEK), 'week']
: seconds < YEAR
? [Math.round(seconds / MONTH), 'month']
: [Math.round(seconds / YEAR), 'year'];
return { value, unit, suffix };
};
class TimeagoFormatter {
}
class TimeagoDefaultFormatter extends TimeagoFormatter {
format(then) {
const { suffix, value, unit } = defaultFormattter(then);
return this.parse(value, unit, suffix);
}
parse(value, unit, suffix) {
if (value !== 1) {
unit += 's';
}
return value + ' ' + unit + ' ' + suffix;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.1.1", ngImport: i0, type: TimeagoDefaultFormatter, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.1.1", ngImport: i0, type: TimeagoDefaultFormatter }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.1.1", ngImport: i0, type: TimeagoDefaultFormatter, decorators: [{
type: Injectable
}] });
class TimeagoCustomFormatter extends TimeagoFormatter {
constructor(intl) {
super();
this.intl = intl;
}
format(then) {
const { suffix, value, unit } = defaultFormattter(then);
return this.parse(value, unit, suffix, Date.now(), then);
}
parse(value, unit, suffix, now, then) {
/** convert weeks to days if strings don't handle weeks */
if (unit === 'week' && !this.intl.strings.week && !this.intl.strings.weeks) {
const days = Math.round(Math.abs(now - then) / (1000 * 60 * 60 * 24));
value = days;
unit = 'day';
}
/** create a normalize function for given value */
const normalize = this.normalizeFn(value, now - then, this.intl.strings.numbers);
/** The eventual return value stored in an array so that the wordSeparator can be used */
const dateString = [];
/** handle prefixes */
if (suffix === 'ago' && this.intl.strings.prefixAgo) {
dateString.push(normalize(this.intl.strings.prefixAgo));
}
if (suffix === 'from now' && this.intl.strings.prefixFromNow) {
dateString.push(normalize(this.intl.strings.prefixFromNow));
}
/** Handle Main number and unit */
const isPlural = value > 1;
if (isPlural) {
const stringFn = this.intl.strings[unit + 's'] || this.intl.strings[unit] || '%d ' + unit;
dateString.push(normalize(stringFn));
}
else {
const stringFn = this.intl.strings[unit] || this.intl.strings[unit + 's'] || '%d ' + unit;
dateString.push(normalize(stringFn));
}
/** Handle Suffixes */
if (suffix === 'ago' && this.intl.strings.suffixAgo) {
dateString.push(normalize(this.intl.strings.suffixAgo));
}
if (suffix === 'from now' && this.intl.strings.suffixFromNow) {
dateString.push(normalize(this.intl.strings.suffixFromNow));
}
/** join the array into a string and return it */
const wordSeparator = typeof this.intl.strings.wordSeparator === 'string' ? this.intl.strings.wordSeparator : ' ';
return dateString.join(wordSeparator);
}
/**
* If the numbers array is present, format numbers with it,
* otherwise just cast the number to a string and return it
*/
normalizeNumber(numbers, value) {
return numbers && numbers.length === 10
? String(value).split('')
.map((digit) => digit.match(/^[0-9]$/) ? numbers[parseInt(digit, 10)] : digit)
.join('')
: String(value);
}
/**
* Take a string or a function that takes number of days and returns a string
* and provide a uniform API to create string parts
*/
normalizeFn(value, millisDelta, numbers) {
return (stringOrFn) => typeof stringOrFn === 'function'
? stringOrFn(value, millisDelta).replace(/%d/g, this.normalizeNumber(numbers, value))
: stringOrFn.replace(/%d/g, this.normalizeNumber(numbers, value));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.1.1", ngImport: i0, type: TimeagoCustomFormatter, deps: [{ token: TimeagoIntl }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.1.1", ngImport: i0, type: TimeagoCustomFormatter }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.1.1", ngImport: i0, type: TimeagoCustomFormatter, decorators: [{
type: Injectable
}], ctorParameters: function () { return [{ type: TimeagoIntl }]; } });
class TimeagoClock {
}
class TimeagoDefaultClock extends TimeagoClock {
tick(then) {
return of(0)
.pipe(expand(() => {
const now = Date.now();
const seconds = Math.round(Math.abs(now - then) / 1000);
const period = seconds < MINUTE
? 1000
: seconds < HOUR
? 1000 * MINUTE
: seconds < DAY
? 1000 * HOUR
: 0;
return period ? timer(period) : empty();
}), skip(1));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.1.1", ngImport: i0, type: TimeagoDefaultClock, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.1.1", ngImport: i0, type: TimeagoDefaultClock }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.1.1", ngImport: i0, type: TimeagoDefaultClock, decorators: [{
type: Injectable
}] });
class TimeagoDirective {
/** The Date to display. An actual Date object or something that can be fed to new Date. */
get date() {
return this._date;
}
set date(date) {
this._date = dateParser(date).valueOf();
if (this._date) {
if (this.clockSubscription) {
this.clockSubscription.unsubscribe();
this.clockSubscription = undefined;
}
this.clockSubscription = this.clock.tick(this.date)
.pipe(filter(() => this.live, this))
.subscribe(() => this.stateChanges.next());
}
else {
throw new SyntaxError(`Wrong parameter in TimeagoDirective. Expected a valid date, received: ${date}`);
}
}
/** If the directive should update itself over time */
get live() {
return this._live;
}
set live(live) {
this._live = coerceBooleanProperty(live);
}
constructor(intl, cd, formatter, element, clock) {
this.cd = cd;
this.clock = clock;
/**
* Emits on:
* - Input change
* - Intl change
* - Clock tick
*/
this.stateChanges = new Subject();
this._live = true;
if (intl) {
this.intlSubscription = intl.changes.subscribe(() => this.stateChanges.next());
}
this.stateChanges.subscribe(() => {
this.setContent(element.nativeElement, formatter.format(this.date));
this.cd.markForCheck();
});
}
ngOnChanges() {
this.stateChanges.next();
}
setContent(node, content) {
if (isDefined(node.textContent)) {
node.textContent = content;
}
else {
node.data = content;
}
}
ngOnDestroy() {
if (this.intlSubscription) {
this.intlSubscription.unsubscribe();
this.intlSubscription = undefined;
}
if (this.clockSubscription) {
this.clockSubscription.unsubscribe();
this.clockSubscription = undefined;
}
this.stateChanges.complete();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.1.1", ngImport: i0, type: TimeagoDirective, deps: [{ token: TimeagoIntl, optional: true }, { token: i0.ChangeDetectorRef }, { token: TimeagoFormatter }, { token: i0.ElementRef }, { token: TimeagoClock }], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.1.1", type: TimeagoDirective, selector: "[timeago]", inputs: { date: "date", live: "live" }, exportAs: ["timeago"], usesOnChanges: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.1.1", ngImport: i0, type: TimeagoDirective, decorators: [{
type: Directive,
args: [{
selector: '[timeago]',
exportAs: 'timeago',
}]
}], ctorParameters: function () { return [{ type: TimeagoIntl, decorators: [{
type: Optional
}] }, { type: i0.ChangeDetectorRef }, { type: TimeagoFormatter }, { type: i0.ElementRef }, { type: TimeagoClock }]; }, propDecorators: { date: [{
type: Input
}], live: [{
type: Input
}] } });
class TimeagoPipe {
constructor(intl, cd, formatter, clock) {
this.clock = clock;
this.live = true;
/**
* Emits on:
* - Input change
* - Intl change
* - Clock tick
*/
this.stateChanges = new Subject();
if (intl) {
this.intlSubscription = intl.changes.subscribe(() => this.stateChanges.next());
}
this.stateChanges.subscribe(() => {
this.value = formatter.format(this.date);
cd.markForCheck();
});
}
transform(date, ...args) {
const _date = dateParser(date).valueOf();
let _live;
_live = isDefined(args[0])
? coerceBooleanProperty(args[0])
: this.live;
if (this.date === _date && this.live === _live) {
return this.value;
}
this.date = _date;
this.live = _live;
if (this.date) {
if (this.clockSubscription) {
this.clockSubscription.unsubscribe();
this.clockSubscription = undefined;
}
this.clockSubscription = this.clock.tick(this.date)
.pipe(filter(() => this.live, this))
.subscribe(() => this.stateChanges.next());
this.stateChanges.next();
}
else {
throw new SyntaxError(`Wrong parameter in TimeagoPipe. Expected a valid date, received: ${date}`);
}
return this.value;
}
ngOnDestroy() {
if (this.intlSubscription) {
this.intlSubscription.unsubscribe();
this.intlSubscription = undefined;
}
if (this.clockSubscription) {
this.clockSubscription.unsubscribe();
this.clockSubscription = undefined;
}
this.stateChanges.complete();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.1.1", ngImport: i0, type: TimeagoPipe, deps: [{ token: TimeagoIntl, optional: true }, { token: i0.ChangeDetectorRef }, { token: TimeagoFormatter }, { token: TimeagoClock }], target: i0.ɵɵFactoryTarget.Pipe }); }
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "16.1.1", ngImport: i0, type: TimeagoPipe, name: "timeago", pure: false }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.1.1", ngImport: i0, type: TimeagoPipe }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.1.1", ngImport: i0, type: TimeagoPipe, decorators: [{
type: Injectable
}, {
type: Pipe,
args: [{
name: 'timeago',
pure: false, // required to update the value when stateChanges emits
}]
}], ctorParameters: function () { return [{ type: TimeagoIntl, decorators: [{
type: Optional
}] }, { type: i0.ChangeDetectorRef }, { type: TimeagoFormatter }, { type: TimeagoClock }]; } });
class TimeagoModule {
/**
* Use this method in your root module to provide the TimeagoModule
*/
static forRoot(config = {}) {
return {
ngModule: TimeagoModule,
providers: [
config.clock || { provide: TimeagoClock, useClass: TimeagoDefaultClock },
config.intl || [],
config.formatter || { provide: TimeagoFormatter, useClass: TimeagoDefaultFormatter },
],
};
}
/**
* Use this method in your other (non root) modules to import the directive/pipe
*/
static forChild(config = {}) {
return {
ngModule: TimeagoModule,
providers: [
config.clock || { provide: TimeagoClock, useClass: TimeagoDefaultClock },
config.intl || [],
config.formatter || { provide: TimeagoFormatter, useClass: TimeagoDefaultFormatter },
],
};
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.1.1", ngImport: i0, type: TimeagoModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "16.1.1", ngImport: i0, type: TimeagoModule, declarations: [TimeagoDirective,
TimeagoPipe], exports: [TimeagoDirective,
TimeagoPipe] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "16.1.1", ngImport: i0, type: TimeagoModule }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.1.1", ngImport: i0, type: TimeagoModule, decorators: [{
type: NgModule,
args: [{
declarations: [
TimeagoDirective,
TimeagoPipe,
],
exports: [
TimeagoDirective,
TimeagoPipe,
],
}]
}] });
/**
* Generated bundle index. Do not edit.
*/
export { TimeagoClock, TimeagoCustomFormatter, TimeagoDefaultClock, TimeagoDefaultFormatter, TimeagoDirective, TimeagoFormatter, TimeagoIntl, TimeagoModule, TimeagoPipe };
//# sourceMappingURL=ngx-timeago.mjs.map