diff --git a/cypress/integration/notyf_spec.js b/cypress/integration/notyf_spec.js index 96dd6a1..ddcc6ea 100644 --- a/cypress/integration/notyf_spec.js +++ b/cypress/integration/notyf_spec.js @@ -54,6 +54,32 @@ context('Notyf', () => { expect(pos.top).to.be.greaterThan(VIEWPORT_HEIGHT / 2); }); }); + + it('should pause on mouseover', () => { + + setConfiguration({ message: 'Notyf 1' }) + cy.get('#success-btn').click() + + setConfiguration({ message: 'Notyf 2' }) + cy.get('#success-btn').click() + + setConfiguration({ message: 'Notyf 3' }) + cy.get('#success-btn').click() + + cy.get('.notyf__toast:nth-child(2)').trigger('mouseover') + + cy.wait(2000) + + cy.get('.notyf').children().should('have.length', 1) + + cy.get('.notyf__toast').trigger('mouseleave') + + cy.wait(2000) + + cy.get('.notyf__toast').should('not.be.exist') + + }) + }); describe('Global custom configuration', () => { diff --git a/src/notyf.models.ts b/src/notyf.models.ts index 943c3b6..05cc328 100644 --- a/src/notyf.models.ts +++ b/src/notyf.models.ts @@ -1,4 +1,5 @@ import { INotyfNotificationOptions, DeepPartial, NotyfEvent } from './notyf.options'; +import EventEmitter from './utils/classes/eventEmitter'; export interface INotyfEventPayload { target: NotyfNotification; @@ -7,20 +8,8 @@ export interface INotyfEventPayload { export type NotyfEventCallback = (payload: INotyfEventPayload) => void; -export class NotyfNotification { - private listeners: Partial> = {}; - - constructor(public options: DeepPartial) {} - - public on(eventType: NotyfEvent, cb: NotyfEventCallback) { - const callbacks = this.listeners[eventType] || []; - this.listeners[eventType] = callbacks.concat([cb]); - } - - private triggerEvent(eventType: NotyfEvent, event?: Event) { - const callbacks = this.listeners[eventType] || []; - callbacks.forEach((cb) => cb({ target: this, event })); - } +export class NotyfNotification extends EventEmitter<{ [ K in NotyfEvent ]: INotyfEventPayload }> { + constructor(public options: DeepPartial) { super() } } export interface IRenderedNotification { diff --git a/src/notyf.options.ts b/src/notyf.options.ts index a498591..54e65de 100644 --- a/src/notyf.options.ts +++ b/src/notyf.options.ts @@ -11,6 +11,8 @@ export interface INotyfPosition { export enum NotyfEvent { Dismiss = 'dismiss', Click = 'click', + MouseOver = 'mouseover', + MouseLeave = 'mouseleave' } export interface INotyfIcon { diff --git a/src/notyf.ts b/src/notyf.ts index c97f929..ea3c8bf 100644 --- a/src/notyf.ts +++ b/src/notyf.ts @@ -7,6 +7,7 @@ import { NotyfEvent, } from './notyf.options'; import { NotyfView } from './notyf.view'; +import Timer from './utils/classes/timer'; /** * Main controller class. Defines the main Notyf API. @@ -29,12 +30,10 @@ export default class Notyf { this.view.on(NotyfEvent.Dismiss, ({ target, event }) => { this._removeNotification(target); - // tslint:disable-next-line: no-string-literal - target['triggerEvent'](NotyfEvent.Dismiss, event); + target.triggerEvent(NotyfEvent.Dismiss, { target, event }); }); - // tslint:disable-next-line: no-string-literal - this.view.on(NotyfEvent.Click, ({ target, event }) => target['triggerEvent'](NotyfEvent.Click, event)); + this.view.on(NotyfEvent.Click, ({ target, event }) => target.triggerEvent(NotyfEvent.Click, { target, event })); } public error(payload: string | Partial) { @@ -82,12 +81,22 @@ export default class Notyf { } private _pushNotification(notification: NotyfNotification) { - this.notifications.push(notification); + + this.notifications.push( notification ); + const duration = notification.options.duration !== undefined ? notification.options.duration : this.options.duration; - if (duration) { - setTimeout(() => this._removeNotification(notification), duration); - } + + if ( !duration ) return + + const timer = new Timer( duration ); + + notification.on(NotyfEvent.MouseOver, () => timer.pause()); + + notification.on(NotyfEvent.MouseLeave, () => timer.resume()); + + timer.on('finished', () => this._removeNotification(notification) ) + } private _removeNotification(notification: NotyfNotification) { diff --git a/src/notyf.view.ts b/src/notyf.view.ts index a1b3942..934c3bf 100644 --- a/src/notyf.view.ts +++ b/src/notyf.view.ts @@ -196,6 +196,14 @@ export class NotyfView { this.events[NotyfEvent.Click]?.({ target: notification, event }), ); + notificationElem.addEventListener('mouseover', event => + notification.triggerEvent(NotyfEvent.MouseOver, { target: notification, event }) + ) + + notificationElem.addEventListener('mouseleave', event => + notification.triggerEvent(NotyfEvent.MouseLeave, { target: notification, event }) + ) + // Adjust margins depending on whether its an upper or lower notification const className = this.getYPosition(options) === 'top' ? 'upper' : 'lower'; notificationElem.classList.add(`notyf__toast--${className}`); diff --git a/src/utils/classes/eventEmitter.ts b/src/utils/classes/eventEmitter.ts new file mode 100644 index 0000000..998f34f --- /dev/null +++ b/src/utils/classes/eventEmitter.ts @@ -0,0 +1,23 @@ +type EventCallback = (event: E) => void; + +export default class EventEmitter< EventMap extends any > { + + protected listeners: { + + [K in keyof EventMap]?: EventCallback< EventMap[K] >[] + + } = {} + + constructor() {} + + on(eventType: Event, cb: EventCallback< EventMap[Event] >) { + const callbacks: EventCallback< EventMap[Event] >[] = this.listeners[eventType] ?? []; + this.listeners[eventType] = callbacks.concat([cb]); + } + + triggerEvent(eventType: Event, event: EventMap[ Event ]) { + const callbacks: EventCallback< EventMap[Event] >[] = this.listeners[eventType] ?? []; + callbacks.forEach((callback: EventCallback< EventMap[Event] >) => callback(event)); + } + +} \ No newline at end of file diff --git a/src/utils/classes/timer.ts b/src/utils/classes/timer.ts new file mode 100644 index 0000000..5a684a8 --- /dev/null +++ b/src/utils/classes/timer.ts @@ -0,0 +1,58 @@ +import EventEmitter from './eventEmitter'; + +type TimerEvents = "finished" | "pause" | "resume" +type TimerEventMap = Record + +export default class Timer extends EventEmitter< TimerEventMap > { + + private startTime: number = Date.now(); + + private timer: ReturnType; + + private lastTime: number = Date.now(); + + get leftTime() { + return this.duration - (this.lastTime - this.startTime); + } + + constructor( public duration: number ) { + + super(); + + this.timer = setTimeout(() => { + + this.triggerEvent('finished', undefined); + + this.lastTime = Date.now(); + + }, duration); + + } + + pause() { + + clearTimeout(this.timer); + + this.lastTime = Date.now(); + + this.triggerEvent('pause', undefined); + + } + + resume() { + + clearTimeout(this.timer); + + this.timer = setTimeout(() => { + + this.triggerEvent('finished', undefined); + + this.lastTime = Date.now(); + + }, this.leftTime); + + this.triggerEvent('resume', undefined); + + } + +}