From 11894307fc86adc77277fc82fdd437c8ae182d18 Mon Sep 17 00:00:00 2001 From: Abrahem Alhofe Date: Tue, 15 Jun 2021 12:33:42 +0200 Subject: [PATCH 1/2] feat( default behaviour ): pause notification kill-duration on mouseover and resume on mouseleave - add "should pause on mouseover" test - add "EventListener", "Timer" utilities' classes - public "NotyfNotification.triggerEvent" | to make "NotyfView" trigger "mouseover" event on "NotyfNotification" - add "NotyfEvent.MouseOver" - add "NotyfEvent.MouseLeave" - update "Notyf._pushNotification" = pause "NotyfNotification" timer and resume timer on "mouseleave" = remove "NotyfNotification" on timer "finished" event - trigger "mouseover" event on "NotyfNotification" by "NotyfView" - trigger "mouseleave" event on "NotyfNotification" by "NotyfView" close #110 --- cypress/integration/notyf_spec.js | 26 +++++++++++++++++ src/notyf.models.ts | 2 +- src/notyf.options.ts | 2 ++ src/notyf.ts | 19 ++++++++++--- src/notyf.view.ts | 8 ++++++ src/utils/classes/eventListener.ts | 17 +++++++++++ src/utils/classes/timer.ts | 45 ++++++++++++++++++++++++++++++ 7 files changed, 114 insertions(+), 5 deletions(-) create mode 100644 src/utils/classes/eventListener.ts create mode 100644 src/utils/classes/timer.ts 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..2e3d09c 100644 --- a/src/notyf.models.ts +++ b/src/notyf.models.ts @@ -17,7 +17,7 @@ export class NotyfNotification { this.listeners[eventType] = callbacks.concat([cb]); } - private triggerEvent(eventType: NotyfEvent, event?: Event) { + triggerEvent(eventType: NotyfEvent, event?: Event) { const callbacks = this.listeners[eventType] || []; callbacks.forEach((cb) => cb({ target: this, event })); } 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..af25712 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. @@ -82,12 +83,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..40a5be9 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, event) + ) + + notificationElem.addEventListener('mouseleave', event => + notification.triggerEvent(NotyfEvent.MouseLeave, 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/eventListener.ts b/src/utils/classes/eventListener.ts new file mode 100644 index 0000000..4478307 --- /dev/null +++ b/src/utils/classes/eventListener.ts @@ -0,0 +1,17 @@ +type EventCallback = (event: any) => void; + +export default class EventeListener { + protected listeners: Partial> = {}; + + constructor() {} + + public on(eventType: string, cb: EventCallback) { + const callbacks = this.listeners[eventType] || []; + this.listeners[eventType] = callbacks.concat([cb]); + } + + protected triggerEvent(eventType: string, event?: any) { + const callbacks = this.listeners[eventType] || []; + callbacks.forEach((callback: EventCallback) => callback(event)); + } +} diff --git a/src/utils/classes/timer.ts b/src/utils/classes/timer.ts new file mode 100644 index 0000000..2a1e586 --- /dev/null +++ b/src/utils/classes/timer.ts @@ -0,0 +1,45 @@ +import EventListener from './eventListener'; + +export default class Timer extends EventListener { + + 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'); + + this.lastTime = Date.now(); + }, duration); + } + + pause() { + this.triggerEvent('pause'); + + clearTimeout(this.timer); + + this.lastTime = Date.now(); + } + + resume() { + this.triggerEvent('resume'); + + clearTimeout(this.timer); + + this.timer = setTimeout(() => { + this.triggerEvent('finished'); + + this.lastTime = Date.now(); + }, this.leftTime); + } + +} From f892ff77699ee8ad1691b4e8ec778b80b5e1a889 Mon Sep 17 00:00:00 2001 From: Abrahem Alhofe Date: Tue, 15 Jun 2021 19:09:17 +0200 Subject: [PATCH 2/2] refactor( EventListener ): improve `EventListener` types - change name of `EventListener" to `EventEmitter` - add `EventMap` generic | `EventMap` includes events and their payloads - make `EventEmitter.targgetEvent` method public - make `Timer` passes their event map to `EventEmitter` - make `NotyfNotification` extends `EventEmitter` - pass `target` as the target property on event "dimiss" and event "click" on `Notyf` - pass `notification` as the target property to events "mouseover" and "mouseleave" on `NotyfView` --- src/notyf.models.ts | 17 +++------------ src/notyf.ts | 6 ++---- src/notyf.view.ts | 4 ++-- src/utils/classes/eventEmitter.ts | 23 +++++++++++++++++++++ src/utils/classes/eventListener.ts | 17 --------------- src/utils/classes/timer.ts | 33 +++++++++++++++++++++--------- 6 files changed, 53 insertions(+), 47 deletions(-) create mode 100644 src/utils/classes/eventEmitter.ts delete mode 100644 src/utils/classes/eventListener.ts diff --git a/src/notyf.models.ts b/src/notyf.models.ts index 2e3d09c..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]); - } - - 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.ts b/src/notyf.ts index af25712..ea3c8bf 100644 --- a/src/notyf.ts +++ b/src/notyf.ts @@ -30,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) { diff --git a/src/notyf.view.ts b/src/notyf.view.ts index 40a5be9..934c3bf 100644 --- a/src/notyf.view.ts +++ b/src/notyf.view.ts @@ -197,11 +197,11 @@ export class NotyfView { ); notificationElem.addEventListener('mouseover', event => - notification.triggerEvent(NotyfEvent.MouseOver, event) + notification.triggerEvent(NotyfEvent.MouseOver, { target: notification, event }) ) notificationElem.addEventListener('mouseleave', event => - notification.triggerEvent(NotyfEvent.MouseLeave, event) + notification.triggerEvent(NotyfEvent.MouseLeave, { target: notification, event }) ) // Adjust margins depending on whether its an upper or lower notification 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/eventListener.ts b/src/utils/classes/eventListener.ts deleted file mode 100644 index 4478307..0000000 --- a/src/utils/classes/eventListener.ts +++ /dev/null @@ -1,17 +0,0 @@ -type EventCallback = (event: any) => void; - -export default class EventeListener { - protected listeners: Partial> = {}; - - constructor() {} - - public on(eventType: string, cb: EventCallback) { - const callbacks = this.listeners[eventType] || []; - this.listeners[eventType] = callbacks.concat([cb]); - } - - protected triggerEvent(eventType: string, event?: any) { - const callbacks = this.listeners[eventType] || []; - callbacks.forEach((callback: EventCallback) => callback(event)); - } -} diff --git a/src/utils/classes/timer.ts b/src/utils/classes/timer.ts index 2a1e586..5a684a8 100644 --- a/src/utils/classes/timer.ts +++ b/src/utils/classes/timer.ts @@ -1,6 +1,9 @@ -import EventListener from './eventListener'; +import EventEmitter from './eventEmitter'; -export default class Timer extends EventListener { +type TimerEvents = "finished" | "pause" | "resume" +type TimerEventMap = Record + +export default class Timer extends EventEmitter< TimerEventMap > { private startTime: number = Date.now(); @@ -12,34 +15,44 @@ export default class Timer extends EventListener { return this.duration - (this.lastTime - this.startTime); } - constructor(public duration: number) { + constructor( public duration: number ) { + super(); this.timer = setTimeout(() => { - this.triggerEvent('finished'); + + this.triggerEvent('finished', undefined); this.lastTime = Date.now(); + }, duration); + } pause() { - this.triggerEvent('pause'); clearTimeout(this.timer); this.lastTime = Date.now(); + + this.triggerEvent('pause', undefined); + } resume() { - this.triggerEvent('resume'); - + clearTimeout(this.timer); - + this.timer = setTimeout(() => { - this.triggerEvent('finished'); - + + this.triggerEvent('finished', undefined); + this.lastTime = Date.now(); + }, this.leftTime); + + this.triggerEvent('resume', undefined); + } }