diff --git a/README.md b/README.md index b343e34..96084e3 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,7 @@ The advanced growl messages component has the following in- and outputs. | life: number (default = 0) | A number that represents the lifetime of each growl message. If set to 3000 each message will be disappear after 3 seconds. If no life param is passed to the components the growl messages are sticky and do not disappear until you call clearMessages or click on the cancel x on a message | |freezeMessagesOnHover: boolean (default: false)| This flag is only useful if you also pass a life time. When you pass this property to the component messages are freezed when you hover over them. Let's say you have for example 3 messages all with a lifetime of 3 seconds. When you hover after 2 seconds over the second message all messages on the screen are freezed and do not disappear. After you leave the messages each message will disappear after the specified lifetime. With the pauseOnlyHoveredMessage you can control if you want all messages or only the hoverd message to disappear| |pauseOnlyHoveredMessage (default: false)| This flag indicates if only the hovered message should be paused. If set to true only the hoverd message will be paused. If set to false all messages will be paused on hover. Notice that this flag is only useful if you set a lifetime that is bigger than 0 and you set the freezeMessagesOnHover flag to true| +|messageSpots (default: 0 (unlimited))| This property gives you the possibility to limit the number of messages displayed on screen. This feature is very useful in combination with the life property. With this combination all messages that can currently not be displayed due to missing message spots will be cached. Those cached messages will appear as soon as a spot is available. Notice that a dynamic change of this property during runtime removes all currently displayed messages from screen| #### Output diff --git a/lib/messages/adv-growl.component.spec.ts b/lib/messages/adv-growl.component.spec.ts index 9a168c7..3ce15b1 100644 --- a/lib/messages/adv-growl.component.spec.ts +++ b/lib/messages/adv-growl.component.spec.ts @@ -10,7 +10,7 @@ import 'rxjs/add/observable/never'; import 'rxjs/add/observable/of'; import 'rxjs/add/observable/throw'; import {AdvPrimeMessage} from './adv-growl.model'; -import {EventEmitter} from '@angular/core'; +import {EventEmitter, SimpleChange} from '@angular/core'; describe('Message Component', () => { @@ -74,8 +74,14 @@ describe('Message Component', () => { observer.next(message); }); }); - spyOn(messagesService, 'getMessageStream').and.returnValue(messages$); - spyOn(messagesService, 'getCancelStream').and.returnValue(Observable.never()); + spyOn(messagesService, 'getMessageStream') + spyOn(messagesService, 'getCancelStream').and.returnValue(Observable.never()) + component.messageCache = { + getMessages: () => { + } + } as any + spyOn(component.messageCache, 'getMessages').and.returnValue(messages$) + // when component.subscribeForMessages(); // then @@ -96,13 +102,22 @@ describe('Message Component', () => { observer.next(message); }); }); - spyOn(messagesService, 'getMessageStream').and.returnValue(messages$); + spyOn(messagesService, 'getMessageStream') spyOn(component, 'getLifeTimeStream').and.returnValue(Observable.of(1)); spyOn(component, 'removeMessage'); + component.messageCache = { + getMessages: () => { + }, + deallocateMessageSpot: () => { + } + } as any + spyOn(component.messageCache, 'getMessages').and.returnValue(messages$) + spyOn(component.messageCache, 'deallocateMessageSpot') // when component.subscribeForMessages(); // then expect(component.removeMessage).toHaveBeenCalledTimes(3); + expect(component.messageCache.deallocateMessageSpot).toHaveBeenCalledTimes(3) }) ); @@ -121,7 +136,7 @@ describe('Message Component', () => { observer.next(message); }); }); - spyOn(messagesService, 'getMessageStream').and.returnValue(messages$); + spyOn(messagesService, 'getMessageStream') spyOn(messagesService, 'getCancelStream').and.callFake(() => { if (numberOfCalls === 0) { numberOfCalls++; @@ -132,10 +147,23 @@ describe('Message Component', () => { spyOn(component, 'getLifeTimeStream').and.returnValue(Observable.of(1)); spyOn(Array.prototype, 'shift'); spyOn(component, 'subscribeForMessages').and.callThrough(); + + component.messageCache = { + getMessages: () => messages$, + clearCache: () => { + }, + deallocateMessageSpot: () => { + } + } as any + + spyOn(component.messageCache, 'clearCache') + spyOn(component.messageCache, 'deallocateMessageSpot') + // when component.subscribeForMessages(); // then expect(component.subscribeForMessages).toHaveBeenCalledTimes(2); + expect(component.messageCache.clearCache).toHaveBeenCalled(); })); it('should remove the message with the matching messageId', () => { @@ -170,9 +198,30 @@ describe('Message Component', () => { const errorMessage = 'Awful error'; const messages$ = Observable.throw(new Error(errorMessage)); spyOn(messagesService, 'getMessageStream').and.returnValue(messages$); + component.messageCache = { + getMessages: () => { + } + } as any + spyOn(component.messageCache, 'getMessages').and.returnValue(messages$) // when then expect(() => component.subscribeForMessages()).toThrowError(errorMessage); - })); + }) + ); + + it('should clear the message cache and resubscribe for messages on spot changes', () => { + // given + component.messageCache = { + getMessages: () => Observable.of('Some message'), + clearCache: () => { + } + } as any + spyOn(component.messageCache, 'clearCache') + // when + component.subscribeForMessages() + component.messageSpotChange$.next() + // then + expect(component.messageCache.clearCache).toHaveBeenCalled() + }) }) describe('Get Life time streams', () => { @@ -337,4 +386,124 @@ describe('Message Component', () => { }) }) }) + + describe('Create message observer', () => { + + it('should create a messageObserver that deallocates messages spots and remove messages on next', () => { + // given + component.ngOnInit() + const messageId = 12345 + spyOn(component.messageCache, 'deallocateMessageSpot') + spyOn(component, 'removeMessage') + // when + const messageObserver = component.createMessageObserver() + messageObserver.next(messageId) + // then + expect(component.messageCache.deallocateMessageSpot).toHaveBeenCalled() + expect(component.removeMessage).toHaveBeenCalledWith(messageId) + }) + + it('should create a messageObserver that calls clear cache and resubscribes on complete', () => { + // given + component.ngOnInit() + spyOn(component.messageCache, 'clearCache') + spyOn(component, 'subscribeForMessages') + // when + const messageObserver = component.createMessageObserver() + messageObserver.complete() + // then + expect(component.messageCache.clearCache).toHaveBeenCalled() + expect(component.subscribeForMessages).toHaveBeenCalled() + }) + }) + + describe('OnChange', () => { + + describe('Have message spots changed', () => { + + it('should return false if the currentValue is null', () => { + // given + const messageSpotChange = { + currentValue: null, + previousValue: 1, + firstChange: false + } as SimpleChange + // when + const hasChanged = component.haveMessageSpotsChanged(messageSpotChange) + // then + expect(hasChanged).toBeFalsy() + }) + + it('should return false if the currentValue is undefined', () => { + // given + const messageSpotChange = { + currentValue: undefined, + previousValue: 1, + firstChange: false + } as SimpleChange + // when + const hasChanged = component.haveMessageSpotsChanged(messageSpotChange) + // then + expect(hasChanged).toBeFalsy() + }) + + it('should return true if the currentValue is 0', () => { + // given + const messageSpotChange = { + currentValue: 0, + previousValue: 1, + firstChange: false + } as SimpleChange + // when + const hasChanged = component.haveMessageSpotsChanged(messageSpotChange) + // then + expect(hasChanged).toBeTruthy() + }) + + it('should return false if it is the first change', () => { + // given + const messageSpotChange = { + currentValue: 0, + previousValue: 1, + firstChange: true + } as SimpleChange + // when + const hasChanged = component.haveMessageSpotsChanged(messageSpotChange) + // then + expect(hasChanged).toBeFalsy() + }) + + it('should return false if it is not the first change but the value has not changed', () => { + // given + const messageSpotChange = { + currentValue: 1, + previousValue: 1, + firstChange: true + } as SimpleChange + // when + const hasChanged = component.haveMessageSpotsChanged(messageSpotChange) + // then + expect(hasChanged).toBeFalsy() + }) + }) + + it('should stream a messageSpot change when the messageSpot has changed', done => { + // given + const messageSpotChange = { + currentValue: 1, + previousValue: 0, + firstChange: false + } as SimpleChange + + const changes = { + messageSpots: messageSpotChange + } + // then + component.messageSpotChange$.subscribe(() => { + done() + }) + // when + component.ngOnChanges(changes) + }) + }) }) diff --git a/lib/messages/adv-growl.component.ts b/lib/messages/adv-growl.component.ts index 9b0eaf8..77e1b99 100644 --- a/lib/messages/adv-growl.component.ts +++ b/lib/messages/adv-growl.component.ts @@ -1,7 +1,18 @@ /** * Created by kevinkreuzer on 08.07.17. */ -import {Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core'; +import { + Component, + ElementRef, + EventEmitter, + Input, + OnChanges, + OnInit, + Output, + SimpleChange, + SimpleChanges, + ViewChild +} from '@angular/core'; import 'rxjs/add/observable/timer'; import 'rxjs/add/operator/do'; import 'rxjs/add/operator/mapTo'; @@ -14,21 +25,26 @@ import {AdvPrimeMessage} from './adv-growl.model'; import {AdvGrowlService} from './adv-growl.service'; import {Subject} from 'rxjs/Subject'; import {AdvGrowlHoverHelper} from './adv-growl.hoverHelper'; +import {AdvGrowlMessageCache} from './adv-growl.messageCache'; +import {Observer} from 'rxjs/Observer'; const DEFAULT_LIFETIME = 0 const FREEZE_MESSAGES_DEFAULT = false const PAUSE_ONLY_HOVERED_DEFAULT = false +const DEFAULT_MESSAGE_SPOTS = 0 @Component({ selector: 'adv-growl', templateUrl: './adv-growl.component.html' }) -export class AdvGrowlComponent implements OnInit { +export class AdvGrowlComponent implements OnInit, OnChanges { + @Input() style: any @Input() styleClass: any @Input('life') lifeTime = DEFAULT_LIFETIME @Input() freezeMessagesOnHover = FREEZE_MESSAGES_DEFAULT + @Input() messageSpots = DEFAULT_MESSAGE_SPOTS @Input() pauseOnlyHoveredMessage = PAUSE_ONLY_HOVERED_DEFAULT; @Output() onClose = new EventEmitter() @Output() onClick = new EventEmitter() @@ -38,33 +54,69 @@ export class AdvGrowlComponent implements OnInit { public messages: Array = [] messageEnter$ = new Subject() + messageSpotChange$ = new Subject() hoverHelper: AdvGrowlHoverHelper; + messageCache: AdvGrowlMessageCache + private messageObserver: Observer constructor(private messageService: AdvGrowlService) { + this.messageObserver = this.createMessageObserver() } ngOnInit(): void { const mouseLeave$ = Observable.fromEvent(this.growlMessage.nativeElement, 'mouseleave') this.hoverHelper = new AdvGrowlHoverHelper(this.messageEnter$, mouseLeave$) + this.messageCache = new AdvGrowlMessageCache() this.subscribeForMessages() } + ngOnChanges(changes: SimpleChanges): void { + const messageSpotChange = changes.messageSpots + if (messageSpotChange != null && this.haveMessageSpotsChanged(messageSpotChange)) { + this.messageSpotChange$.next() + } + } + + haveMessageSpotsChanged(messageSpotChange: SimpleChange) { + const currentValue = messageSpotChange.currentValue + const previousValue = messageSpotChange.previousValue + const firstChange = messageSpotChange.firstChange + const hasValueChanged = currentValue !== previousValue + if (currentValue != null && !firstChange && hasValueChanged) { + return true + } + return false + } + + createMessageObserver(): Observer { + return { + next: (messageId: string) => { + this.messageCache.deallocateMessageSpot() + this.removeMessage(messageId) + }, + error: (error) => { + throw error; + }, + complete: () => { + this.messageCache.clearCache() + this.subscribeForMessages() + } + } + } + public subscribeForMessages() { this.messages = []; - this.messageService.getMessageStream() + this.messageCache.getMessages(this.messageService.getMessageStream(), this.messageSpots) .do(message => { this.messages.push(message); this.onMessagesChanges.emit(this.messages); }) .mergeMap(message => this.getLifeTimeStream(message.id)) - .takeUntil(this.messageService.getCancelStream()) - .subscribe( - messageId => this.removeMessage(messageId), - err => { - throw err; - }, - () => this.subscribeForMessages() - ); + .takeUntil(Observable.merge( + this.messageService.getCancelStream(), + this.messageSpotChange$) + ) + .subscribe(this.messageObserver); } removeMessage(messageId: string) { diff --git a/lib/messages/adv-growl.messageCache.spec.ts b/lib/messages/adv-growl.messageCache.spec.ts new file mode 100644 index 0000000..b61d290 --- /dev/null +++ b/lib/messages/adv-growl.messageCache.spec.ts @@ -0,0 +1,198 @@ +/** + * Created by kevinkreuzer on 16.10.17. + */ +import {AdvGrowlMessageCache, MESSAGE_SENDER} from './adv-growl.messageCache'; +import {Observable} from 'rxjs/Observable'; + +describe('AdvGrowl Message Cache', () => { + + const maxNumberOfMessages = 5 + let sut + + beforeEach(() => { + sut = new AdvGrowlMessageCache() + }) + + describe('Cache interactions', () => { + + it('should detect that the cache is empty if no elements are cached', () => { + // given + sut.messageCache = [] + // when + const isCacheEmpty = sut.isCacheEmpty() + // then + expect(isCacheEmpty).toBeTruthy() + }) + + it('should detect that the cache is not empty if there are elements cached', () => { + // given + sut.messageCache = ['AwesomeMessage1', 'AwesomeMessage2'] as any + // when + const isCacheEmpty = sut.isCacheEmpty() + // then + expect(isCacheEmpty).toBeFalsy() + }) + + it('should clear the cache and deallocate all message spots on clearCache()', () => { + // given + sut.messageCache = ['AwesomeMessage1', 'AwesomeMessage2'] as any + sut.allocatedMessageSpots = 2 + // when + sut.clearCache() + // then + expect(sut.messageCache).toEqual([]) + expect(sut.allocatedMessageSpots).toBe(0) + }) + }) + + describe('Deallocating message spots', () => { + + it('should deallocate a messagespot and stream a cached message', () => { + // given + const allocatedMessageSpots = 5 + const expectedAllocatedMessageSpots = 4 + const awesomeSuperMessage = 'Awesome super message' + const normalMessage = 'Normal message' + sut.messageCache = [awesomeSuperMessage, normalMessage] as any + sut.allocatedMessageSpots = allocatedMessageSpots + + spyOn(sut, 'isCacheEmpty').and.returnValue(false) + // when + sut.deallocateMessageSpot() + // then + expect(sut.allocatedMessageSpots).toBe(expectedAllocatedMessageSpots) + sut.cachedMessage$.subscribe(message => { + expect(message).toEqual({sender: MESSAGE_SENDER.CACHE, message: awesomeSuperMessage}) + }) + }) + + it('should deallocate a messagespot and schredder the message if nothing is cached', () => { + // given + const allocatedMessageSpots = 1 + const expectedAllocatedMessageSpots = 0 + sut.messageCache = [] + sut.allocatedMessageSpots = allocatedMessageSpots + + spyOn(sut, 'isCacheEmpty').and.returnValue(true) + // when + sut.deallocateMessageSpot() + // then + expect(sut.allocatedMessageSpots).toBe(expectedAllocatedMessageSpots) + sut.schredder$.subscribe(message => { + expect(message).toEqual({sender: MESSAGE_SENDER.SCHREDDER}) + }) + }) + }) + + describe('Get user messages', () => { + + it('should cache messgaes and wait if there are no more spots available', () => { + // given + const message = 'Awesome message' as any + const messageWithSender = {sender: MESSAGE_SENDER.USER, message: message} + sut.messageSpots = maxNumberOfMessages + sut.allocatedMessageSpots = maxNumberOfMessages + + spyOn(Observable, 'never') + + // when + sut.getUserMessage(messageWithSender) + // then + expect(sut.messageCache).toEqual([message]) + expect(Observable.never).toHaveBeenCalled() + }) + + it('should allocate a spot for a cached message', () => { + // given + const message = 'Awesome message' as any + const messageWithSender = {sender: MESSAGE_SENDER.USER, message: message} + sut.allocatedMessageSpots = maxNumberOfMessages - 1 + // when + const usermessage$ = sut.getUserMessage(messageWithSender) + // then + expect(sut.allocatedMessageSpots).toBe(maxNumberOfMessages) + usermessage$.subscribe(userMessage => expect(userMessage).toBe(message)) + }) + }) + + describe('Get message', () => { + + it('should call getUserMessage if the sender is the user', () => { + // given + const messageWithSender = {sender: MESSAGE_SENDER.USER, message: 'Awesome message' as any} + spyOn(sut, 'getUserMessage') + // when + sut.getMessage(messageWithSender) + // then + expect(sut.getUserMessage).toHaveBeenCalledWith(messageWithSender) + }) + + it('should allocate a spot and return a stream with the message if the sender is the cache', () => { + // given + const message = 'Awesome message' as any + const messageWithSender = {sender: MESSAGE_SENDER.CACHE, message: message} + const allocatedSpots = 3 + sut.allocatedMessageSpots = allocatedSpots + // when + const message$ = sut.getMessage(messageWithSender) + // then + expect(sut.allocatedMessageSpots).toBe(allocatedSpots + 1) + message$.subscribe(messageFromCache => expect(messageFromCache).toBe(message)) + }) + + it('should return a stream that never completes if the schredderer is the sender', () => { + // given + const messageWithSender = {sender: MESSAGE_SENDER.SCHREDDER} + spyOn(Observable, 'never') + // when + sut.getMessage(messageWithSender) + // then + expect(Observable.never).toHaveBeenCalled() + }) + }) + + describe('Get messages', () => { + + it('should return a message if the message Stream emitts a value', () => { + // given + const message = 'Awesome message' + const message$ = Observable.of(message as any) + + spyOn(Observable.prototype, 'switchMap').and.returnValue(Observable.of(message)) + + // when + const messages$ = sut.getMessages(message$, maxNumberOfMessages) + // then + messages$.subscribe(m => expect(m).toBe(message)) + }) + + it('should call switchMap if the cached message Stream emitts a value', () => { + // given + const message = 'Awesome message' + const message$ = Observable.empty() + + spyOn(Observable.prototype, 'switchMap').and.returnValue(Observable.never()) + + // when + sut.getMessages(message$) + sut.cachedMessage$.next(message as any) + // then + expect(Observable.prototype.switchMap).toHaveBeenCalled() + }) + + it('should call switchMap if the schredder message Stream emitts a value', () => { + // given + const message = 'Awesome message' + const message$ = Observable.empty() + + spyOn(Observable.prototype, 'switchMap').and.returnValue(Observable.never()) + + // when + sut.getMessages(message$, maxNumberOfMessages) + sut.schredder$.next(message as any) + // then + expect(Observable.prototype.switchMap).toHaveBeenCalled() + }) + }) +}) + diff --git a/lib/messages/adv-growl.messageCache.ts b/lib/messages/adv-growl.messageCache.ts new file mode 100644 index 0000000..063c744 --- /dev/null +++ b/lib/messages/adv-growl.messageCache.ts @@ -0,0 +1,94 @@ +/** + * Created by kevinkreuzer on 16.10.17. + */ +import {AdvPrimeMessage} from './adv-growl.model'; +import {Observable} from 'rxjs/Observable'; +import {Subject} from 'rxjs/Subject'; +import 'rxjs/add/operator/map' +import 'rxjs/add/operator/scan' +import 'rxjs/add/observable/of' + +export enum MESSAGE_SENDER { + USER, + CACHE, + SCHREDDER +} + +interface MessageWithSender { + sender: MESSAGE_SENDER, + message?: AdvPrimeMessage +} + +export class AdvGrowlMessageCache { + + messageCache: Array = [] + cachedMessage$ = new Subject() + schredder$ = new Subject() + allocatedMessageSpots: number + hasMessageSpots: boolean + messageSpots: number + + constructor() { + } + + public getMessages(message$: Observable, messageSpots: number): Observable { + this.messageSpots = messageSpots + this.hasMessageSpots = messageSpots !== 0 + this.allocatedMessageSpots = 0 + + if (!this.hasMessageSpots) { + return message$ + } + + return Observable.merge( + message$.map((message: AdvPrimeMessage) => ({ + sender: MESSAGE_SENDER.USER, + message: message + })), + this.cachedMessage$, + this.schredder$ + ) + .switchMap(this.getMessage) + } + + getMessage = (messageWithSender: MessageWithSender): Observable => { + switch (messageWithSender.sender) { + case MESSAGE_SENDER.USER: + return this.getUserMessage(messageWithSender) + case MESSAGE_SENDER.CACHE: + this.allocatedMessageSpots++ + return Observable.of(messageWithSender.message) + case MESSAGE_SENDER.SCHREDDER: + return Observable.never() + } + } + + getUserMessage(messageWithSender: MessageWithSender): Observable { + if (this.allocatedMessageSpots >= this.messageSpots) { + this.messageCache.push(messageWithSender.message) + return Observable.never() + } else { + this.allocatedMessageSpots++ + return Observable.of(messageWithSender.message) + } + } + + deallocateMessageSpot(): void { + this.allocatedMessageSpots-- + if (this.isCacheEmpty()) { + this.schredder$.next({sender: MESSAGE_SENDER.SCHREDDER}) + } else { + const message = this.messageCache.shift() + this.cachedMessage$.next({sender: MESSAGE_SENDER.CACHE, message: message}) + } + } + + isCacheEmpty(): boolean { + return this.messageCache.length === 0 + } + + public clearCache(): void { + this.allocatedMessageSpots = 0 + this.messageCache = [] + } +} diff --git a/package.json b/package.json index eb04667..5c53a06 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "primeng-advanced-growl", - "version": "2.4.1", + "version": "2.5.0", "description": "The AdvGrowlModule is a wrapper around the growl module from PrimeNG. This wrapper was created because PrimeNG is missing some features.", "keywords": [ "PrimeNG, Angular, RxJS, Growl, Messages" @@ -59,7 +59,7 @@ "@types/jasminewd2": "~2.0.2", "@types/node": "~8.0.28", "angular2-uuid": "^1.1.1", - "codecov": "^2.2.0", + "codecov": "^3.0.0", "codelyzer": "~3.2.0", "font-awesome": "^4.7.0", "jasmine-core": "~2.8.0", @@ -71,10 +71,9 @@ "karma-jasmine": "~1.1.0", "karma-jasmine-html-reporter": "^0.2.2", "primeng": "^4.2.1", - "protractor": "~5.1.2", "rxjs": "^5.1.0", "ts-node": "~3.3.0", - "tslint": "~5.7.0", + "tslint": "~5.8.0", "typescript": "=2.3.4", "zone.js": "0.8.18" } diff --git a/test/app/app.component.html b/test/app/app.component.html index a0635ed..798435f 100644 --- a/test/app/app.component.html +++ b/test/app/app.component.html @@ -13,6 +13,7 @@
3. Choose if you want to freeze only the hoverd message
+
+

4. Choose if you want to limit the number of message displayed

+ (default: 0 (no limitations)) +
+
+ This property gives you the possibility to limit the number of messages displayed on screen. This feature is very useful in combination with the life property. With this combination all messages that can currently not be displayed due to missing message spots will be cached. Those cached messages will appear as soon as a spot is available. Notice that a dynamic change of this property during runtime removes all currently displayed messages from screen + +
+
+ + +
+
-

3. Lets create some messages

+

5. Lets create some messages


Next let's create some messages.

@@ -126,7 +140,7 @@

3. Lets create some messages

-

4. More Options - Extra functionalities

+

6. More Options - Extra functionalities


diff --git a/test/app/app.component.ts b/test/app/app.component.ts index 428d94f..3ae33fc 100644 --- a/test/app/app.component.ts +++ b/test/app/app.component.ts @@ -2,6 +2,9 @@ import {Component} from '@angular/core'; import {AdvGrowlService} from '../../lib/messages/adv-growl.service'; import {AdvPrimeMessage} from '../../lib/messages/adv-growl.model'; +const DEFAULT_MESSAGE_SPOTS = 0 +const DEFAULT_LIFETIME = 0 + @Component({ selector: 'app-root', templateUrl: './app.component.html', @@ -10,7 +13,8 @@ import {AdvPrimeMessage} from '../../lib/messages/adv-growl.model'; export class AppComponent { messages = [] - lifeTime = 0 + lifeTime = DEFAULT_LIFETIME + messageSpots = DEFAULT_MESSAGE_SPOTS freezeMessagesOnHover = false pauseOnlyHoveredMessage = false version = require('../../package.json').version