@ng-idle/core
Version:
A module for detecting and responding to the user becoming idle in Angular applications.
926 lines (906 loc) • 28.7 kB
JavaScript
import * as i0 from '@angular/core';
import { Injectable, EventEmitter, PLATFORM_ID, Optional, Inject, NgModule } from '@angular/core';
import { isPlatformServer } from '@angular/common';
import { Subscription, fromEvent, merge } from 'rxjs';
import { filter, throttleTime } from 'rxjs/operators';
/*
* A class for managing an interrupt from an interrupt source.
*/
class Interrupt {
constructor(source, options) {
this.source = source;
if (source.initialize) {
source.initialize(options);
}
}
/*
* Subscribes to the interrupt using the specified function.
* @param fn - The subscription function.
*/
subscribe(fn) {
this.sub = this.source.onInterrupt.subscribe(fn);
}
/*
* Unsubscribes the interrupt.
*/
unsubscribe() {
this.sub.unsubscribe();
this.sub = null;
}
/*
* Keeps the subscription but resumes interrupt events.
*/
resume() {
this.source.attach();
}
/*
* Keeps the subscription but pauses interrupt events.
*/
pause() {
this.source.detach();
}
}
/*
* Represents a base class for types that provide expiry detection for the Idle service.
*/
class IdleExpiry {
constructor() {
this.idValue = new Date();
this.idlingValue = false;
}
/*
* Gets or sets a unique ID for the window
* @param id - The id.
* @return The current id.
*/
id(value) {
if (value !== void 0) {
if (!value) {
throw new Error('A value must be specified for the ID.');
}
this.idValue = value;
}
return this.idValue;
}
/*
* Gets or sets the idling value.
* @param value - The value to set.
* @return The idling value.
*/
idling(value) {
if (value !== void 0) {
this.idlingValue = value;
}
return this.idlingValue;
}
/*
* Returns the current Date.
* @return The current Date.
*/
now() {
/* istanbul ignore next */
return new Date();
}
/*
* Returns whether or not it is expired.
* @return True if expired; otherwise, false.
*/
isExpired() {
const expiry = this.last();
return expiry != null && expiry <= this.now();
}
}
/*
* Represents an alternative storage for browser that doesn't support localstorage. (i.e. Safari in
* private mode)
* @implements Storage
*/
class AlternativeStorage {
constructor() {
this.storageMap = {};
}
/*
* Returns an integer representing the number of data items stored in the storageMap object.
*/
get length() {
return Object.keys(this.storageMap).length;
}
/*
* Remove all keys out of the storage.
*/
clear() {
this.storageMap = {};
}
/*
* Return the key's value
*
* @param key - name of the key to retrieve the value of.
* @return The key's value
*/
getItem(key) {
if (typeof this.storageMap[key] !== 'undefined') {
return this.storageMap[key];
}
return null;
}
/*
* Return the nth key in the storage
*
* @param index - the number of the key you want to get the name of.
* @return The name of the key.
*/
key(index) {
return Object.keys(this.storageMap)[index] || null;
}
/*
* Remove a key from the storage.
*
* @param key - the name of the key you want to remove.
*/
removeItem(key) {
this.storageMap[key] = undefined;
}
/*
* Add a key to the storage, or update a key's value if it already exists.
*
* @param key - the name of the key.
* @param value - the value you want to give to the key.
*/
setItem(key, value) {
this.storageMap[key] = value;
}
}
/*
* Represents a localStorage store.
*/
class LocalStorage {
constructor() {
this.storage = this.getStorage();
}
/*
* Safari, in Private Browsing Mode, looks like it supports localStorage but all calls to setItem
* throw QuotaExceededError. We're going to detect this and just silently drop any calls to
* setItem
* to avoid the entire page breaking, without having to do a check at each usage of Storage.
*/
getStorage() {
try {
const storage = localStorage;
storage.setItem('ng2IdleStorage', '');
storage.removeItem('ng2IdleStorage');
return storage;
}
catch (err) {
return new AlternativeStorage();
}
}
/*
* Gets an item in the storage.
*
* @param value - The value to get.
* @return The current value.
*/
getItem(key) {
return this.storage.getItem('ng2Idle.' + key);
}
/*
* Removes an item in the storage.
*
* @param value - The value to remove.
*/
removeItem(key) {
this.storage.removeItem('ng2Idle.' + key);
}
/*
* Sets an item in the storage.
*
* @param key - The key to set the value.
* @param value - The value to set to the key.
*/
setItem(key, data) {
this.storage.setItem('ng2Idle.' + key, data);
}
/*
* Represents the storage, commonly use for testing purposes.
*
* @param key - The key to set the value.
* @param value - The value to set to the key.
*/
_wrapped() {
return this.storage;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: LocalStorage, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: LocalStorage }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: LocalStorage, decorators: [{
type: Injectable
}], ctorParameters: () => [] });
/*
* Represents a localStorage store of expiry values.
* @extends IdleExpiry
*/
class LocalStorageExpiry extends IdleExpiry {
constructor(localStorage) {
super();
this.localStorage = localStorage;
this.idleName = 'main';
}
/*
* Gets or sets the last expiry date in localStorage.
* If localStorage doesn't work correctly (i.e. Safari in private mode), we store the expiry value in memory.
* @param value - The expiry value to set; omit to only return the value.
* @return The current expiry value.
*/
last(value) {
if (value !== void 0) {
this.setExpiry(value);
}
return this.getExpiry();
}
idling(value) {
if (value !== void 0) {
this.setIdling(value);
}
return this.getIdling();
}
/*
* Gets the idle name.
* @return The name of the idle.
*/
getIdleName() {
return this.idleName;
}
/*
* Sets the idle name.
* @param The name of the idle.
*/
setIdleName(key) {
if (key) {
this.idleName = key;
}
}
getExpiry() {
const expiry = this.localStorage.getItem(this.idleName + '.expiry');
if (expiry) {
return new Date(parseInt(expiry, 10));
}
else {
return null;
}
}
setExpiry(value) {
if (value) {
this.localStorage.setItem(this.idleName + '.expiry', value.getTime().toString());
}
else {
this.localStorage.removeItem(this.idleName + '.expiry');
}
}
getIdling() {
const idling = this.localStorage.getItem(this.idleName + '.idling');
if (idling) {
return idling === 'true';
}
else {
return false;
}
}
setIdling(value) {
if (value) {
this.localStorage.setItem(this.idleName + '.idling', value.toString());
}
else {
this.localStorage.setItem(this.idleName + '.idling', 'false');
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: LocalStorageExpiry, deps: [{ token: LocalStorage }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: LocalStorageExpiry }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: LocalStorageExpiry, decorators: [{
type: Injectable
}], ctorParameters: () => [{ type: LocalStorage }] });
class KeepaliveSvc {
}
/*
* Indicates the desired auto resume behavior.
*/
var AutoResume;
(function (AutoResume) {
/*
* Auto resume functionality will be disabled.
*/
AutoResume[AutoResume["disabled"] = 0] = "disabled";
/*
* Can resume automatically even if they are idle.
*/
AutoResume[AutoResume["idle"] = 1] = "idle";
/*
* Can only resume automatically if they are not yet idle.
*/
AutoResume[AutoResume["notIdle"] = 2] = "notIdle";
})(AutoResume || (AutoResume = {}));
/**
* A service for detecting and responding to user idleness.
*/
class Idle {
constructor(expiry, zone, keepaliveSvc, platformId) {
this.expiry = expiry;
this.zone = zone;
this.platformId = platformId;
this.idle = 20 * 60; // in seconds
this.timeoutVal = 30; // in seconds
this.autoResume = AutoResume.idle;
this.interrupts = new Array();
this.running = false;
this.keepaliveEnabled = false;
this.onIdleStart = new EventEmitter();
this.onIdleEnd = new EventEmitter();
this.onTimeoutWarning = new EventEmitter();
this.onTimeout = new EventEmitter();
this.onInterrupt = new EventEmitter();
if (keepaliveSvc) {
this.keepaliveSvc = keepaliveSvc;
this.keepaliveEnabled = true;
}
this.setIdling(false);
}
/*
* Sets the idle name for localStorage.
* Important to set if multiple instances of Idle with LocalStorageExpiry
* @param The name of the idle.
*/
setIdleName(key) {
if (this.expiry instanceof LocalStorageExpiry) {
this.expiry.setIdleName(key);
}
else {
throw new Error('Cannot set expiry key name because no LocalStorageExpiry has been provided.');
}
}
/*
* Returns whether or not keepalive integration is enabled.
* @return True if integration is enabled; otherwise, false.
*/
getKeepaliveEnabled() {
return this.keepaliveEnabled;
}
/*
* Sets and returns whether or not keepalive integration is enabled.
* @param True if the integration is enabled; otherwise, false.
* @return The current value.
*/
setKeepaliveEnabled(value) {
if (!this.keepaliveSvc) {
throw new Error('Cannot enable keepalive integration because no KeepaliveSvc has been provided.');
}
return (this.keepaliveEnabled = value);
}
/*
* Returns the current timeout value.
* @return The timeout value in seconds.
*/
getTimeout() {
return this.timeoutVal;
}
/*
* Sets the timeout value.
* @param seconds - The timeout value in seconds. 0 or false to disable timeout feature.
* @return The current value. If disabled, the value will be 0.
*/
setTimeout(seconds) {
if (seconds === false) {
this.timeoutVal = 0;
}
else if (typeof seconds === 'number' && seconds >= 0) {
this.timeoutVal = seconds;
}
else {
throw new Error("'seconds' can only be 'false' or a positive number.");
}
return this.timeoutVal;
}
/*
* Returns the current idle value.
* @return The idle value in seconds.
*/
getIdle() {
return this.idle;
}
/*
* Sets the idle value.
* @param seconds - The idle value in seconds.
* @return The idle value in seconds.
*/
setIdle(seconds) {
if (seconds <= 0) {
throw new Error("'seconds' must be greater zero");
}
return (this.idle = seconds);
}
/*
* Returns the current autoresume value.
* @return The current value.
*/
getAutoResume() {
return this.autoResume;
}
setAutoResume(value) {
return (this.autoResume = value);
}
/*
* Sets interrupts from the specified sources.
* @param sources - Interrupt sources.
* @return The resulting interrupts.
*/
setInterrupts(sources) {
this.clearInterrupts();
const self = this;
for (const source of sources) {
const options = { platformId: this.platformId };
const sub = new Interrupt(source, options);
sub.subscribe((args) => {
self.interrupt(args.force, args.innerArgs);
});
this.interrupts.push(sub);
}
return this.interrupts;
}
/*
* Returns the current interrupts.
* @return The current interrupts.
*/
getInterrupts() {
return this.interrupts;
}
/*
* Pauses, unsubscribes, and clears the current interrupt subscriptions.
*/
clearInterrupts() {
for (const sub of this.interrupts) {
sub.pause();
sub.unsubscribe();
}
this.interrupts.length = 0;
}
/*
* Returns whether or not the service is running i.e. watching for idleness.
* @return True if service is watching; otherwise, false.
*/
isRunning() {
return this.running;
}
/*
* Returns whether or not the user is considered idle.
* @return True if the user is in the idle state; otherwise, false.
*/
isIdling() {
return this.idling;
}
/*
* Starts watching for inactivity.
*/
watch(skipExpiry) {
this.safeClearInterval('idleHandle');
this.safeClearInterval('timeoutHandle');
const timeout = !this.timeoutVal ? 0 : this.timeoutVal;
if (!skipExpiry) {
const value = new Date(this.expiry.now().getTime() + (this.idle + timeout) * 1000);
this.expiry.last(value);
}
if (this.idling) {
this.toggleState();
}
if (!this.running) {
this.startKeepalive();
this.toggleInterrupts(true);
}
this.running = true;
const watchFn = () => {
this.zone.run(() => {
const diff = this.getExpiryDiff(timeout);
if (diff > 0) {
this.safeClearInterval('idleHandle');
this.setIdleIntervalOutsideOfZone(watchFn, 1000);
}
else {
this.toggleState();
}
});
};
this.setIdleIntervalOutsideOfZone(watchFn, 1000);
}
/*
* Allows protractor tests to call waitForAngular without hanging
*/
setIdleIntervalOutsideOfZone(watchFn, frequency) {
this.zone.runOutsideAngular(() => {
this.idleHandle = setInterval(watchFn, frequency);
});
}
/*
* Stops watching for inactivity.
*/
stop() {
this.stopKeepalive();
this.toggleInterrupts(false);
this.safeClearInterval('idleHandle');
this.safeClearInterval('timeoutHandle');
this.setIdling(false);
this.running = false;
this.expiry.last(null);
}
/*
* Forces a timeout event and state.
*/
timeout() {
this.stopKeepalive();
this.toggleInterrupts(false);
this.safeClearInterval('idleHandle');
this.safeClearInterval('timeoutHandle');
this.setIdling(true);
this.running = false;
this.countdown = 0;
this.onTimeout.emit(null);
}
/*
* Signals that user activity has occurred.
* @param force - Forces watch to be called, unless they are timed out.
* @param eventArgs - Optional source event arguments.
*/
interrupt(force, eventArgs) {
if (!this.running) {
return;
}
if (this.timeoutVal && this.expiry.isExpired()) {
this.timeout();
return;
}
this.onInterrupt.emit(eventArgs);
if (force === true ||
this.autoResume === AutoResume.idle ||
(this.autoResume === AutoResume.notIdle && !this.expiry.idling())) {
this.watch(force);
}
}
setIdling(value) {
this.idling = value;
this.expiry.idling(value);
}
toggleState() {
this.setIdling(!this.idling);
if (this.idling) {
this.onIdleStart.emit(null);
this.stopKeepalive();
if (this.timeoutVal > 0) {
this.countdown = this.timeoutVal;
this.doCountdown();
this.setTimeoutIntervalOutsideZone(() => {
this.doCountdownInZone();
}, 1000);
}
}
else {
this.toggleInterrupts(true);
this.onIdleEnd.emit(null);
this.startKeepalive();
}
this.safeClearInterval('idleHandle');
}
setTimeoutIntervalOutsideZone(intervalFn, frequency) {
this.zone.runOutsideAngular(() => {
this.timeoutHandle = setInterval(() => {
intervalFn();
}, frequency);
});
}
toggleInterrupts(resume) {
for (const interrupt of this.interrupts) {
if (resume) {
interrupt.resume();
}
else {
interrupt.pause();
}
}
}
getExpiryDiff(timeout) {
const now = this.expiry.now();
const last = this.expiry.last() || now;
return last.getTime() - now.getTime() - timeout * 1000;
}
doCountdownInZone() {
this.zone.run(() => {
this.doCountdown();
});
}
doCountdown() {
const diff = this.getExpiryDiff(this.timeoutVal);
if (diff > 0) {
this.safeClearInterval('timeoutHandle');
this.interrupt(true);
return;
}
if (!this.idling) {
return;
}
if (this.countdown <= 0) {
this.timeout();
return;
}
this.onTimeoutWarning.emit(this.countdown);
const countdownMs = ((this.timeoutVal - 1) * 1000) + diff;
this.countdown = Math.round(countdownMs / 1000);
}
safeClearInterval(handleName) {
const handle = this[handleName];
if (handle !== null && typeof handle !== 'undefined') {
clearInterval(this[handleName]);
this[handleName] = null;
}
}
startKeepalive() {
if (!this.keepaliveSvc || !this.keepaliveEnabled) {
return;
}
if (this.running) {
this.keepaliveSvc.ping();
}
this.keepaliveSvc.start();
}
stopKeepalive() {
if (!this.keepaliveSvc || !this.keepaliveEnabled) {
return;
}
this.keepaliveSvc.stop();
}
/*
* Called by Angular when destroying the instance.
*/
ngOnDestroy() {
this.stop();
this.clearInterrupts();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: Idle, deps: [{ token: IdleExpiry }, { token: i0.NgZone }, { token: KeepaliveSvc, optional: true }, { token: PLATFORM_ID, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: Idle }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: Idle, decorators: [{
type: Injectable
}], ctorParameters: () => [{ type: IdleExpiry }, { type: i0.NgZone }, { type: KeepaliveSvc, decorators: [{
type: Optional
}] }, { type: Object, decorators: [{
type: Optional
}, {
type: Inject,
args: [PLATFORM_ID]
}] }] });
/*
* A class for expressing arguments to interrupt events.
*/
class InterruptArgs {
constructor(source, innerArgs, force = false) {
this.source = source;
this.innerArgs = innerArgs;
this.force = force;
}
}
/*
* A base for classes that act as a source for interrupts.
*/
class InterruptSource {
constructor(attachFn, detachFn) {
this.attachFn = attachFn;
this.detachFn = detachFn;
this.isAttached = false;
this.onInterrupt = new EventEmitter();
}
/*
* Attaches to the specified events on the specified source.
*/
attach() {
// If the current zone is the 'angular' zone (a.k.a. NgZone) then re-enter this method in its parent zone
// The parent zone is usually the '<root>' zone but it can also be 'long-stack-trace-zone' in debug mode
// In tests, the current zone is typically a 'ProxyZone' created by async/fakeAsync (from @angular/core/testing)
if (Zone.current.get('isAngularZone') === true) {
Zone.current.parent.run(() => this.attach());
return;
}
if (!this.isAttached && this.attachFn) {
this.attachFn(this);
}
this.isAttached = true;
}
/*
* Detaches from the specified events on the specified source.
*/
detach() {
if (this.isAttached && this.detachFn) {
this.detachFn(this);
}
this.isAttached = false;
}
}
const defaultThrottleDelay = 500;
/*
* An interrupt source on an EventTarget object, such as a Window or HTMLElement.
*/
class EventTargetInterruptSource extends InterruptSource {
constructor(target, events, opts) {
super(null, null);
this.target = target;
this.events = events;
this.opts = opts;
this.eventSubscription = new Subscription();
if (typeof this.opts === 'number') {
this.opts = { throttleDelay: this.opts, passive: false };
}
this.opts = this.opts || {
passive: false,
throttleDelay: defaultThrottleDelay
};
if (this.opts.throttleDelay === undefined || this.opts.throttleDelay === null) {
this.opts.throttleDelay = defaultThrottleDelay;
}
this.throttleDelay = this.opts.throttleDelay;
this.passive = !!this.opts.passive;
}
initialize(options) {
if (options?.platformId && isPlatformServer(options.platformId)) {
return;
}
const eventTarget = typeof this.target === 'function' ? this.target() : this.target;
const opts = this.passive ? { passive: true } : null;
const fromEvents = this.events
.split(' ')
.map(eventName => fromEvent(eventTarget, eventName, opts));
this.eventSrc = merge(...fromEvents);
this.eventSrc = this.eventSrc.pipe(filter(innerArgs => !this.filterEvent(innerArgs)));
if (this.throttleDelay > 0) {
this.eventSrc = this.eventSrc.pipe(throttleTime(this.throttleDelay));
}
const handler = (innerArgs) => this.onInterrupt.emit(new InterruptArgs(this, innerArgs));
this.attachFn = () => (this.eventSubscription = this.eventSrc.subscribe(handler));
this.detachFn = () => this.eventSubscription.unsubscribe();
}
/*
* Checks to see if the event should be filtered. Always returns false unless overriden.
* @param event - The original event object.
* @return True if the event should be filtered (don't cause an interrupt); otherwise, false.
*/
filterEvent(event) {
return false;
}
/**
* Returns the current options being used.
* @return The current option values.
*/
get options() {
return {
passive: this.passive,
throttleDelay: this.throttleDelay
};
}
}
/*
* An interrupt source that uses events on the document element (html tag).
*/
class DocumentInterruptSource extends EventTargetInterruptSource {
constructor(events, options) {
super(() => document.documentElement, events, options);
}
/*
* Checks to see if the event should be filtered.
* @param event - The original event object.
* @return True if the event should be filtered (don't cause an interrupt); otherwise, false.
*/
filterEvent(event) {
// some browser bad input hacks
if (event.type === 'mousemove' &&
// fix for Chrome destop notifications
((event.originalEvent &&
event.originalEvent.movementX === 0 &&
event.originalEvent.movementY === 0) ||
// fix for webkit fake mousemove
((event.movementX !== void 0 && !event.movementX) || !event.movementY))) {
return true;
}
return false;
}
}
/*
* An interrupt source on the Window object.
*/
class WindowInterruptSource extends EventTargetInterruptSource {
constructor(events, options) {
super(() => window, events, options);
}
}
/*
* An interrupt source on the storage event of Window.
*/
class StorageInterruptSource extends WindowInterruptSource {
constructor(options = 500) {
super('storage', options);
}
/*
* Checks to see if the event should be filtered.
* @param event - The original event object.
* @return True if the event should be filtered (don't cause an interrupt); otherwise, false.
*/
filterEvent(event) {
if (event.key &&
event.key.indexOf('ng2Idle.') >= 0 &&
event.key.indexOf('.expiry') >= 0) {
return false;
}
return true;
}
}
/*
* Represents a simple in-memory store of expiry values.
* @extends IdleExpiry
*/
class SimpleExpiry extends IdleExpiry {
constructor() {
super();
this.lastValue = null;
}
/*
* Gets or sets the last expiry date.
* @param value - The expiry value to set; omit to only return the value.
* @return The current expiry value.
*/
last(value) {
if (value !== void 0) {
this.lastValue = value;
}
return this.lastValue;
}
}
function provideNgIdle() {
return [
LocalStorage,
LocalStorageExpiry,
Idle,
{
provide: IdleExpiry,
useExisting: LocalStorageExpiry
}
];
}
;
class NgIdleModule {
static forRoot() {
return {
ngModule: NgIdleModule,
providers: [
LocalStorageExpiry,
{ provide: IdleExpiry, useExisting: LocalStorageExpiry },
Idle
]
};
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: NgIdleModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "18.2.5", ngImport: i0, type: NgIdleModule }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: NgIdleModule, providers: [LocalStorage] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: NgIdleModule, decorators: [{
type: NgModule,
args: [{
providers: [LocalStorage]
}]
}] });
function createDefaultInterruptSources(options) {
return [
new DocumentInterruptSource('mousemove keydown DOMMouseScroll mousewheel mousedown touchstart touchmove scroll', options),
new StorageInterruptSource(options)
];
}
const DEFAULT_INTERRUPTSOURCES = createDefaultInterruptSources();
/*
* Public API Surface of core
*/
/**
* Generated bundle index. Do not edit.
*/
export { AutoResume, DEFAULT_INTERRUPTSOURCES, DocumentInterruptSource, EventTargetInterruptSource, Idle, IdleExpiry, InterruptArgs, InterruptSource, KeepaliveSvc, LocalStorage, LocalStorageExpiry, NgIdleModule, SimpleExpiry, StorageInterruptSource, WindowInterruptSource, createDefaultInterruptSources, provideNgIdle };
//# sourceMappingURL=ng-idle-core.mjs.map