add event dispatching system for Livewire integration

Implement consistent event dispatching across Noty, Notyf, Toastr adapters and
themes, following the existing SweetAlert pattern. This enables Livewire
integration for all notification types.

JavaScript Events:
- Noty: flasher:noty:show, flasher:noty:click, flasher:noty:close, flasher:noty:hover
- Notyf: flasher:notyf:click, flasher:notyf:dismiss
- Toastr: flasher:toastr:show, flasher:toastr:click, flasher:toastr:close, flasher:toastr:hidden
- Themes: flasher:theme:click (generic), flasher:theme:{name}:click (specific)

PHP Livewire Listeners:
- LivewireListener for each adapter (Noty, Notyf, Toastr)
- ThemeLivewireListener for theme click events
- Registered in service providers when Livewire is bound

This allows Livewire users to listen for notification events and react
accordingly (e.g., noty:click, theme:flasher:click).
This commit is contained in:
Younes ENNAJI
2026-03-01 21:05:10 +00:00
parent f1051e1d7f
commit 7d6e9b46b8
42 changed files with 1342 additions and 15 deletions
@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace Flasher\Laravel\EventListener;
use Flasher\Prime\EventDispatcher\Event\ResponseEvent;
use Flasher\Prime\EventDispatcher\EventListener\EventListenerInterface;
final readonly class ThemeLivewireListener implements EventListenerInterface
{
public function __invoke(ResponseEvent $event): void
{
// Only process HTML responses
if ('html' !== $event->getPresenter()) {
return;
}
$response = $event->getResponse() ?: '';
if (!\is_string($response)) {
return;
}
// Avoid duplicate script injection
if (false === strripos($response, '<script type="text/javascript" class="flasher-js"')) {
return;
}
if (strripos($response, '<script type="text/javascript" class="flasher-theme-livewire-js"')) {
return;
}
// Inject the Theme-Livewire bridge JavaScript
$response .= <<<'JAVASCRIPT'
<script type="text/javascript" class="flasher-theme-livewire-js">
(function() {
window.addEventListener('flasher:theme:click', function(event) {
if (typeof Livewire === 'undefined') {
return;
}
const { detail } = event;
const { envelope } = detail;
const context = envelope.context || {};
if (!context.livewire?.id) {
return;
}
const { livewire: { id: componentId } } = context;
const component = Livewire.all().find(c => c.id === componentId);
if (!component) {
return;
}
Livewire.dispatchTo(component.name, 'theme:click', { payload: detail });
// Also dispatch theme-specific event
const plugin = envelope.metadata?.plugin || '';
let themeName = plugin;
if (plugin.startsWith('theme.')) {
themeName = plugin.replace('theme.', '');
}
if (themeName) {
Livewire.dispatchTo(component.name, 'theme:' + themeName + ':click', { payload: detail });
}
}, false);
})();
</script>
JAVASCRIPT;
$event->setResponse($response);
}
public function getSubscribedEvents(): string
{
return ResponseEvent::class;
}
}
+13
View File
@@ -8,6 +8,7 @@ use Flasher\Laravel\Command\InstallCommand;
use Flasher\Laravel\Component\FlasherComponent;
use Flasher\Laravel\EventListener\LivewireListener;
use Flasher\Laravel\EventListener\OctaneListener;
use Flasher\Laravel\EventListener\ThemeLivewireListener;
use Flasher\Laravel\Middleware\FlasherMiddleware;
use Flasher\Laravel\Middleware\SessionMiddleware;
use Flasher\Laravel\Storage\SessionBag;
@@ -17,6 +18,7 @@ use Flasher\Laravel\Translation\Translator;
use Flasher\Prime\Asset\AssetManager;
use Flasher\Prime\Container\FlasherContainer;
use Flasher\Prime\EventDispatcher\EventDispatcher;
use Flasher\Prime\EventDispatcher\EventDispatcherInterface;
use Flasher\Prime\EventDispatcher\EventListener\ApplyPresetListener;
use Flasher\Prime\EventDispatcher\EventListener\NotificationLoggerListener;
use Flasher\Prime\EventDispatcher\EventListener\TranslationListener;
@@ -317,5 +319,16 @@ final class FlasherServiceProvider extends PluginServiceProvider
$livewire->listen('dehydrate', new LivewireListener($livewire, $flasher, $cspHandler, $request));
});
$this->registerThemeLivewireListener();
}
private function registerThemeLivewireListener(): void
{
$this->app->extend('flasher.event_dispatcher', static function (EventDispatcherInterface $dispatcher) {
$dispatcher->addListener(new ThemeLivewireListener());
return $dispatcher;
});
}
}
@@ -6,6 +6,7 @@ namespace Flasher\Noty\Laravel;
use Flasher\Laravel\Support\PluginServiceProvider;
use Flasher\Noty\Prime\NotyPlugin;
use Flasher\Prime\EventDispatcher\EventDispatcherInterface;
final class FlasherNotyServiceProvider extends PluginServiceProvider
{
@@ -13,4 +14,22 @@ final class FlasherNotyServiceProvider extends PluginServiceProvider
{
return new NotyPlugin();
}
protected function afterBoot(): void
{
$this->registerLivewireListener();
}
private function registerLivewireListener(): void
{
if (!$this->app->bound('livewire')) {
return;
}
$this->app->extend('flasher.event_dispatcher', static function (EventDispatcherInterface $dispatcher) {
$dispatcher->addListener(new LivewireListener());
return $dispatcher;
});
}
}
+75
View File
@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace Flasher\Noty\Laravel;
use Flasher\Prime\EventDispatcher\Event\ResponseEvent;
use Flasher\Prime\EventDispatcher\EventListener\EventListenerInterface;
final readonly class LivewireListener implements EventListenerInterface
{
public function __invoke(ResponseEvent $event): void
{
// Only process HTML responses
if ('html' !== $event->getPresenter()) {
return;
}
$response = $event->getResponse() ?: '';
if (!\is_string($response)) {
return;
}
// Avoid duplicate script injection
if (false === strripos($response, '<script type="text/javascript" class="flasher-js"')) {
return;
}
if (strripos($response, '<script type="text/javascript" class="flasher-noty-livewire-js"')) {
return;
}
// Inject the Noty-Livewire bridge JavaScript
$response .= <<<'JAVASCRIPT'
<script type="text/javascript" class="flasher-noty-livewire-js">
(function() {
const events = ['flasher:noty:show', 'flasher:noty:click', 'flasher:noty:close', 'flasher:noty:hover'];
events.forEach(function(eventName) {
window.addEventListener(eventName, function(event) {
if (typeof Livewire === 'undefined') {
return;
}
const { detail } = event;
const { envelope } = detail;
const context = envelope.context || {};
if (!context.livewire?.id) {
return;
}
const { livewire: { id: componentId } } = context;
const component = Livewire.all().find(c => c.id === componentId);
if (!component) {
return;
}
const livewireEventName = eventName.replace('flasher:', '').replace(':', ':');
Livewire.dispatchTo(component.name, livewireEventName, { payload: detail });
}, false);
});
})();
</script>
JAVASCRIPT;
$event->setResponse($response);
}
public function getSubscribedEvents(): string
{
return ResponseEvent::class;
}
}
+34
View File
@@ -29,6 +29,34 @@ export default class NotyPlugin extends AbstractPlugin {
Object.assign(options, envelope.options)
}
// Wrap callbacks to dispatch events
const originalCallbacks = {
onShow: options.callbacks?.onShow,
onClick: options.callbacks?.onClick,
onClose: options.callbacks?.onClose,
onHover: options.callbacks?.onHover,
}
options.callbacks = {
...options.callbacks,
onShow: () => {
this.dispatchEvent('flasher:noty:show', envelope)
originalCallbacks.onShow?.()
},
onClick: () => {
this.dispatchEvent('flasher:noty:click', envelope)
originalCallbacks.onClick?.()
},
onClose: () => {
this.dispatchEvent('flasher:noty:close', envelope)
originalCallbacks.onClose?.()
},
onHover: () => {
this.dispatchEvent('flasher:noty:hover', envelope)
originalCallbacks.onHover?.()
},
}
const noty = new Noty(options)
noty.show()
@@ -42,6 +70,12 @@ export default class NotyPlugin extends AbstractPlugin {
})
}
private dispatchEvent(eventName: string, envelope: Envelope): void {
window.dispatchEvent(new CustomEvent(eventName, {
detail: { envelope },
}))
}
public renderOptions(options: Options): void {
if (!options) {
return
+29
View File
@@ -96,11 +96,35 @@ class NotyPlugin extends AbstractPlugin {
return;
}
envelopes.forEach((envelope) => {
var _a, _b, _c, _d;
try {
const options = Object.assign({ text: envelope.message, type: envelope.type }, this.defaultOptions);
if (envelope.options) {
Object.assign(options, envelope.options);
}
const originalCallbacks = {
onShow: (_a = options.callbacks) === null || _a === void 0 ? void 0 : _a.onShow,
onClick: (_b = options.callbacks) === null || _b === void 0 ? void 0 : _b.onClick,
onClose: (_c = options.callbacks) === null || _c === void 0 ? void 0 : _c.onClose,
onHover: (_d = options.callbacks) === null || _d === void 0 ? void 0 : _d.onHover,
};
options.callbacks = Object.assign(Object.assign({}, options.callbacks), { onShow: () => {
var _a;
this.dispatchEvent('flasher:noty:show', envelope);
(_a = originalCallbacks.onShow) === null || _a === void 0 ? void 0 : _a.call(originalCallbacks);
}, onClick: () => {
var _a;
this.dispatchEvent('flasher:noty:click', envelope);
(_a = originalCallbacks.onClick) === null || _a === void 0 ? void 0 : _a.call(originalCallbacks);
}, onClose: () => {
var _a;
this.dispatchEvent('flasher:noty:close', envelope);
(_a = originalCallbacks.onClose) === null || _a === void 0 ? void 0 : _a.call(originalCallbacks);
}, onHover: () => {
var _a;
this.dispatchEvent('flasher:noty:hover', envelope);
(_a = originalCallbacks.onHover) === null || _a === void 0 ? void 0 : _a.call(originalCallbacks);
} });
const noty = new Noty(options);
noty.show();
const layoutDom = noty.layoutDom;
@@ -113,6 +137,11 @@ class NotyPlugin extends AbstractPlugin {
}
});
}
dispatchEvent(eventName, envelope) {
window.dispatchEvent(new CustomEvent(eventName, {
detail: { envelope },
}));
}
renderOptions(options) {
if (!options) {
return;
+29
View File
@@ -99,11 +99,35 @@
return;
}
envelopes.forEach((envelope) => {
var _a, _b, _c, _d;
try {
const options = Object.assign({ text: envelope.message, type: envelope.type }, this.defaultOptions);
if (envelope.options) {
Object.assign(options, envelope.options);
}
const originalCallbacks = {
onShow: (_a = options.callbacks) === null || _a === void 0 ? void 0 : _a.onShow,
onClick: (_b = options.callbacks) === null || _b === void 0 ? void 0 : _b.onClick,
onClose: (_c = options.callbacks) === null || _c === void 0 ? void 0 : _c.onClose,
onHover: (_d = options.callbacks) === null || _d === void 0 ? void 0 : _d.onHover,
};
options.callbacks = Object.assign(Object.assign({}, options.callbacks), { onShow: () => {
var _a;
this.dispatchEvent('flasher:noty:show', envelope);
(_a = originalCallbacks.onShow) === null || _a === void 0 ? void 0 : _a.call(originalCallbacks);
}, onClick: () => {
var _a;
this.dispatchEvent('flasher:noty:click', envelope);
(_a = originalCallbacks.onClick) === null || _a === void 0 ? void 0 : _a.call(originalCallbacks);
}, onClose: () => {
var _a;
this.dispatchEvent('flasher:noty:close', envelope);
(_a = originalCallbacks.onClose) === null || _a === void 0 ? void 0 : _a.call(originalCallbacks);
}, onHover: () => {
var _a;
this.dispatchEvent('flasher:noty:hover', envelope);
(_a = originalCallbacks.onHover) === null || _a === void 0 ? void 0 : _a.call(originalCallbacks);
} });
const noty = new Noty(options);
noty.show();
const layoutDom = noty.layoutDom;
@@ -116,6 +140,11 @@
}
});
}
dispatchEvent(eventName, envelope) {
window.dispatchEvent(new CustomEvent(eventName, {
detail: { envelope },
}));
}
renderOptions(options) {
if (!options) {
return;
+1 -1
View File
@@ -1 +1 @@
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("@flasher/flasher"),require("noty")):"function"==typeof define&&define.amd?define(["@flasher/flasher","noty"],t):(e="undefined"!=typeof globalThis?globalThis:e||self).Noty=t(e.flasher,e.Noty)}(this,(function(e,t){"use strict";class s{success(e,t,s){this.flash("success",e,t,s)}error(e,t,s){this.flash("error",e,t,s)}info(e,t,s){this.flash("info",e,t,s)}warning(e,t,s){this.flash("warning",e,t,s)}flash(e,t,s,o){let i,n,r,l={};if("object"==typeof e?(l=Object.assign({},e),i=l.type,n=l.message,r=l.title,delete l.type,delete l.message,delete l.title):"object"==typeof t?(l=Object.assign({},t),i=e,n=l.message,r=l.title,delete l.message,delete l.title):(i=e,n=t,null==s?(r=void 0,l=o||{}):"string"==typeof s?(r=s,l=o||{}):"object"==typeof s&&(l=Object.assign({},s),"title"in l?(r=l.title,delete l.title):r=void 0,o&&"object"==typeof o&&(l=Object.assign(Object.assign({},l),o)))),!i)throw new Error("Type is required for notifications");if(null==n)throw new Error("Message is required for notifications");null==r&&(r=i.charAt(0).toUpperCase()+i.slice(1));const a={type:i,message:n,title:r,options:l,metadata:{plugin:""}};this.renderOptions({}),this.renderEnvelopes([a])}}const o=new class extends s{constructor(){super(...arguments),this.defaultOptions={timeout:1e4}}renderEnvelopes(e){(null==e?void 0:e.length)&&e.forEach((e=>{try{const s=Object.assign({text:e.message,type:e.type},this.defaultOptions);e.options&&Object.assign(s,e.options);const o=new t(s);o.show();const i=o.layoutDom;i&&"object"==typeof i.dataset&&(i.dataset.turboTemporary="")}catch(t){console.error("PHPFlasher Noty: Error rendering notification",t,e)}}))}renderOptions(e){e&&(Object.assign(this.defaultOptions,e),t.overrideDefaults(this.defaultOptions))}};return e.addPlugin("noty",o),o}));
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("@flasher/flasher"),require("noty")):"function"==typeof define&&define.amd?define(["@flasher/flasher","noty"],t):(e="undefined"!=typeof globalThis?globalThis:e||self).Noty=t(e.flasher,e.Noty)}(this,(function(e,t){"use strict";class o{success(e,t,o){this.flash("success",e,t,o)}error(e,t,o){this.flash("error",e,t,o)}info(e,t,o){this.flash("info",e,t,o)}warning(e,t,o){this.flash("warning",e,t,o)}flash(e,t,o,s){let n,i,l,a={};if("object"==typeof e?(a=Object.assign({},e),n=a.type,i=a.message,l=a.title,delete a.type,delete a.message,delete a.title):"object"==typeof t?(a=Object.assign({},t),n=e,i=a.message,l=a.title,delete a.message,delete a.title):(n=e,i=t,null==o?(l=void 0,a=s||{}):"string"==typeof o?(l=o,a=s||{}):"object"==typeof o&&(a=Object.assign({},o),"title"in a?(l=a.title,delete a.title):l=void 0,s&&"object"==typeof s&&(a=Object.assign(Object.assign({},a),s)))),!n)throw new Error("Type is required for notifications");if(null==i)throw new Error("Message is required for notifications");null==l&&(l=n.charAt(0).toUpperCase()+n.slice(1));const r={type:n,message:i,title:l,options:a,metadata:{plugin:""}};this.renderOptions({}),this.renderEnvelopes([r])}}const s=new class extends o{constructor(){super(...arguments),this.defaultOptions={timeout:1e4}}renderEnvelopes(e){(null==e?void 0:e.length)&&e.forEach((e=>{var o,s,n,i;try{const l=Object.assign({text:e.message,type:e.type},this.defaultOptions);e.options&&Object.assign(l,e.options);const a={onShow:null===(o=l.callbacks)||void 0===o?void 0:o.onShow,onClick:null===(s=l.callbacks)||void 0===s?void 0:s.onClick,onClose:null===(n=l.callbacks)||void 0===n?void 0:n.onClose,onHover:null===(i=l.callbacks)||void 0===i?void 0:i.onHover};l.callbacks=Object.assign(Object.assign({},l.callbacks),{onShow:()=>{var t;this.dispatchEvent("flasher:noty:show",e),null===(t=a.onShow)||void 0===t||t.call(a)},onClick:()=>{var t;this.dispatchEvent("flasher:noty:click",e),null===(t=a.onClick)||void 0===t||t.call(a)},onClose:()=>{var t;this.dispatchEvent("flasher:noty:close",e),null===(t=a.onClose)||void 0===t||t.call(a)},onHover:()=>{var t;this.dispatchEvent("flasher:noty:hover",e),null===(t=a.onHover)||void 0===t||t.call(a)}});const r=new t(l);r.show();const c=r.layoutDom;c&&"object"==typeof c.dataset&&(c.dataset.turboTemporary="")}catch(t){console.error("PHPFlasher Noty: Error rendering notification",t,e)}}))}dispatchEvent(e,t){window.dispatchEvent(new CustomEvent(e,{detail:{envelope:t}}))}renderOptions(e){e&&(Object.assign(this.defaultOptions,e),t.overrideDefaults(this.defaultOptions))}};return e.addPlugin("noty",s),s}));
+1
View File
@@ -3,5 +3,6 @@ import type { Envelope, Options } from '@flasher/flasher/dist/types';
export default class NotyPlugin extends AbstractPlugin {
private defaultOptions;
renderEnvelopes(envelopes: Envelope[]): void;
private dispatchEvent;
renderOptions(options: Options): void;
}
+1 -1
View File
@@ -1 +1 @@
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("@flasher/flasher"),require("noty")):"function"==typeof define&&define.amd?define(["@flasher/flasher","noty"],t):(e="undefined"!=typeof globalThis?globalThis:e||self).Noty=t(e.flasher,e.Noty)}(this,(function(e,t){"use strict";class s{success(e,t,s){this.flash("success",e,t,s)}error(e,t,s){this.flash("error",e,t,s)}info(e,t,s){this.flash("info",e,t,s)}warning(e,t,s){this.flash("warning",e,t,s)}flash(e,t,s,o){let i,n,r,l={};if("object"==typeof e?(l=Object.assign({},e),i=l.type,n=l.message,r=l.title,delete l.type,delete l.message,delete l.title):"object"==typeof t?(l=Object.assign({},t),i=e,n=l.message,r=l.title,delete l.message,delete l.title):(i=e,n=t,null==s?(r=void 0,l=o||{}):"string"==typeof s?(r=s,l=o||{}):"object"==typeof s&&(l=Object.assign({},s),"title"in l?(r=l.title,delete l.title):r=void 0,o&&"object"==typeof o&&(l=Object.assign(Object.assign({},l),o)))),!i)throw new Error("Type is required for notifications");if(null==n)throw new Error("Message is required for notifications");null==r&&(r=i.charAt(0).toUpperCase()+i.slice(1));const a={type:i,message:n,title:r,options:l,metadata:{plugin:""}};this.renderOptions({}),this.renderEnvelopes([a])}}const o=new class extends s{constructor(){super(...arguments),this.defaultOptions={timeout:1e4}}renderEnvelopes(e){(null==e?void 0:e.length)&&e.forEach((e=>{try{const s=Object.assign({text:e.message,type:e.type},this.defaultOptions);e.options&&Object.assign(s,e.options);const o=new t(s);o.show();const i=o.layoutDom;i&&"object"==typeof i.dataset&&(i.dataset.turboTemporary="")}catch(t){console.error("PHPFlasher Noty: Error rendering notification",t,e)}}))}renderOptions(e){e&&(Object.assign(this.defaultOptions,e),t.overrideDefaults(this.defaultOptions))}};return e.addPlugin("noty",o),o}));
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("@flasher/flasher"),require("noty")):"function"==typeof define&&define.amd?define(["@flasher/flasher","noty"],t):(e="undefined"!=typeof globalThis?globalThis:e||self).Noty=t(e.flasher,e.Noty)}(this,(function(e,t){"use strict";class o{success(e,t,o){this.flash("success",e,t,o)}error(e,t,o){this.flash("error",e,t,o)}info(e,t,o){this.flash("info",e,t,o)}warning(e,t,o){this.flash("warning",e,t,o)}flash(e,t,o,s){let n,i,l,a={};if("object"==typeof e?(a=Object.assign({},e),n=a.type,i=a.message,l=a.title,delete a.type,delete a.message,delete a.title):"object"==typeof t?(a=Object.assign({},t),n=e,i=a.message,l=a.title,delete a.message,delete a.title):(n=e,i=t,null==o?(l=void 0,a=s||{}):"string"==typeof o?(l=o,a=s||{}):"object"==typeof o&&(a=Object.assign({},o),"title"in a?(l=a.title,delete a.title):l=void 0,s&&"object"==typeof s&&(a=Object.assign(Object.assign({},a),s)))),!n)throw new Error("Type is required for notifications");if(null==i)throw new Error("Message is required for notifications");null==l&&(l=n.charAt(0).toUpperCase()+n.slice(1));const r={type:n,message:i,title:l,options:a,metadata:{plugin:""}};this.renderOptions({}),this.renderEnvelopes([r])}}const s=new class extends o{constructor(){super(...arguments),this.defaultOptions={timeout:1e4}}renderEnvelopes(e){(null==e?void 0:e.length)&&e.forEach((e=>{var o,s,n,i;try{const l=Object.assign({text:e.message,type:e.type},this.defaultOptions);e.options&&Object.assign(l,e.options);const a={onShow:null===(o=l.callbacks)||void 0===o?void 0:o.onShow,onClick:null===(s=l.callbacks)||void 0===s?void 0:s.onClick,onClose:null===(n=l.callbacks)||void 0===n?void 0:n.onClose,onHover:null===(i=l.callbacks)||void 0===i?void 0:i.onHover};l.callbacks=Object.assign(Object.assign({},l.callbacks),{onShow:()=>{var t;this.dispatchEvent("flasher:noty:show",e),null===(t=a.onShow)||void 0===t||t.call(a)},onClick:()=>{var t;this.dispatchEvent("flasher:noty:click",e),null===(t=a.onClick)||void 0===t||t.call(a)},onClose:()=>{var t;this.dispatchEvent("flasher:noty:close",e),null===(t=a.onClose)||void 0===t||t.call(a)},onHover:()=>{var t;this.dispatchEvent("flasher:noty:hover",e),null===(t=a.onHover)||void 0===t||t.call(a)}});const r=new t(l);r.show();const c=r.layoutDom;c&&"object"==typeof c.dataset&&(c.dataset.turboTemporary="")}catch(t){console.error("PHPFlasher Noty: Error rendering notification",t,e)}}))}dispatchEvent(e,t){window.dispatchEvent(new CustomEvent(e,{detail:{envelope:t}}))}renderOptions(e){e&&(Object.assign(this.defaultOptions,e),t.overrideDefaults(this.defaultOptions))}};return e.addPlugin("noty",s),s}));
@@ -6,6 +6,7 @@ namespace Flasher\Notyf\Laravel;
use Flasher\Laravel\Support\PluginServiceProvider;
use Flasher\Notyf\Prime\NotyfPlugin;
use Flasher\Prime\EventDispatcher\EventDispatcherInterface;
final class FlasherNotyfServiceProvider extends PluginServiceProvider
{
@@ -13,4 +14,22 @@ final class FlasherNotyfServiceProvider extends PluginServiceProvider
{
return new NotyfPlugin();
}
protected function afterBoot(): void
{
$this->registerLivewireListener();
}
private function registerLivewireListener(): void
{
if (!$this->app->bound('livewire')) {
return;
}
$this->app->extend('flasher.event_dispatcher', static function (EventDispatcherInterface $dispatcher) {
$dispatcher->addListener(new LivewireListener());
return $dispatcher;
});
}
}
+75
View File
@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace Flasher\Notyf\Laravel;
use Flasher\Prime\EventDispatcher\Event\ResponseEvent;
use Flasher\Prime\EventDispatcher\EventListener\EventListenerInterface;
final readonly class LivewireListener implements EventListenerInterface
{
public function __invoke(ResponseEvent $event): void
{
// Only process HTML responses
if ('html' !== $event->getPresenter()) {
return;
}
$response = $event->getResponse() ?: '';
if (!\is_string($response)) {
return;
}
// Avoid duplicate script injection
if (false === strripos($response, '<script type="text/javascript" class="flasher-js"')) {
return;
}
if (strripos($response, '<script type="text/javascript" class="flasher-notyf-livewire-js"')) {
return;
}
// Inject the Notyf-Livewire bridge JavaScript
$response .= <<<'JAVASCRIPT'
<script type="text/javascript" class="flasher-notyf-livewire-js">
(function() {
const events = ['flasher:notyf:click', 'flasher:notyf:dismiss'];
events.forEach(function(eventName) {
window.addEventListener(eventName, function(event) {
if (typeof Livewire === 'undefined') {
return;
}
const { detail } = event;
const { envelope } = detail;
const context = envelope.context || {};
if (!context.livewire?.id) {
return;
}
const { livewire: { id: componentId } } = context;
const component = Livewire.all().find(c => c.id === componentId);
if (!component) {
return;
}
const livewireEventName = eventName.replace('flasher:', '').replace(':', ':');
Livewire.dispatchTo(component.name, livewireEventName, { payload: detail });
}, false);
});
})();
</script>
JAVASCRIPT;
$event->setResponse($response);
}
public function getSubscribedEvents(): string
{
return ResponseEvent::class;
}
}
+36 -1
View File
@@ -16,7 +16,11 @@ export default class NotyfPlugin extends AbstractPlugin {
envelopes.forEach((envelope) => {
try {
const options = { ...envelope, ...envelope.options }
this.notyf?.open(options)
const notification = this.notyf?.open(options)
if (notification) {
this.attachEventListeners(notification, envelope)
}
} catch (error) {
console.error('PHPFlasher Notyf: Error rendering notification', error, envelope)
}
@@ -92,4 +96,35 @@ export default class NotyfPlugin extends AbstractPlugin {
types.push(newType)
}
}
private attachEventListeners(notification: unknown, envelope: Envelope): void {
if (!this.notyf) {
return
}
// Notyf supports events at runtime but types don't include them
const notyf = this.notyf as unknown as {
on: (event: string, callback: (params: { target: unknown, event: Event }) => void) => void
}
// Listen for click events
notyf.on('click', ({ target, event }) => {
if (target === notification) {
this.dispatchEvent('flasher:notyf:click', envelope, { event })
}
})
// Listen for dismiss events
notyf.on('dismiss', ({ target, event }) => {
if (target === notification) {
this.dispatchEvent('flasher:notyf:dismiss', envelope, { event })
}
})
}
private dispatchEvent(eventName: string, envelope: Envelope, extra: Record<string, any> = {}): void {
window.dispatchEvent(new CustomEvent(eventName, {
detail: { envelope, ...extra },
}))
}
}
+25 -1
View File
@@ -515,7 +515,10 @@ class NotyfPlugin extends AbstractPlugin {
var _a;
try {
const options = Object.assign(Object.assign({}, envelope), envelope.options);
(_a = this.notyf) === null || _a === void 0 ? void 0 : _a.open(options);
const notification = (_a = this.notyf) === null || _a === void 0 ? void 0 : _a.open(options);
if (notification) {
this.attachEventListeners(notification, envelope);
}
}
catch (error) {
console.error('PHPFlasher Notyf: Error rendering notification', error, envelope);
@@ -579,6 +582,27 @@ class NotyfPlugin extends AbstractPlugin {
types.push(newType);
}
}
attachEventListeners(notification, envelope) {
if (!this.notyf) {
return;
}
const notyf = this.notyf;
notyf.on('click', ({ target, event }) => {
if (target === notification) {
this.dispatchEvent('flasher:notyf:click', envelope, { event });
}
});
notyf.on('dismiss', ({ target, event }) => {
if (target === notification) {
this.dispatchEvent('flasher:notyf:dismiss', envelope, { event });
}
});
}
dispatchEvent(eventName, envelope, extra = {}) {
window.dispatchEvent(new CustomEvent(eventName, {
detail: Object.assign({ envelope }, extra),
}));
}
}
const notyf = new NotyfPlugin();
+25 -1
View File
@@ -519,7 +519,10 @@
var _a;
try {
const options = Object.assign(Object.assign({}, envelope), envelope.options);
(_a = this.notyf) === null || _a === void 0 ? void 0 : _a.open(options);
const notification = (_a = this.notyf) === null || _a === void 0 ? void 0 : _a.open(options);
if (notification) {
this.attachEventListeners(notification, envelope);
}
}
catch (error) {
console.error('PHPFlasher Notyf: Error rendering notification', error, envelope);
@@ -583,6 +586,27 @@
types.push(newType);
}
}
attachEventListeners(notification, envelope) {
if (!this.notyf) {
return;
}
const notyf = this.notyf;
notyf.on('click', ({ target, event }) => {
if (target === notification) {
this.dispatchEvent('flasher:notyf:click', envelope, { event });
}
});
notyf.on('dismiss', ({ target, event }) => {
if (target === notification) {
this.dispatchEvent('flasher:notyf:dismiss', envelope, { event });
}
});
}
dispatchEvent(eventName, envelope, extra = {}) {
window.dispatchEvent(new CustomEvent(eventName, {
detail: Object.assign({ envelope }, extra),
}));
}
}
const notyf = new NotyfPlugin();
File diff suppressed because one or more lines are too long
+2
View File
@@ -7,4 +7,6 @@ export default class NotyfPlugin extends AbstractPlugin {
renderOptions(options: Options): void;
private initializeNotyf;
private addTypeIfNotExists;
private attachEventListeners;
private dispatchEvent;
}
File diff suppressed because one or more lines are too long
@@ -168,6 +168,15 @@ export default class FlasherPlugin extends AbstractPlugin {
})
}
// Add click event listener to dispatch theme events
notification.addEventListener('click', (event) => {
// Don't trigger if clicking the close button
if ((event.target as HTMLElement).closest('.fl-close')) {
return
}
this.dispatchClickEvents(envelope)
})
// Add timer if timeout is greater than 0 (not sticky)
if (options.timeout > 0) {
this.addTimer(notification, options)
@@ -312,4 +321,33 @@ export default class FlasherPlugin extends AbstractPlugin {
return str.replace(/[&<>"'`=/]/g, (char) => htmlEscapes[char] || char)
}
private dispatchClickEvents(envelope: Envelope): void {
const detail = { envelope }
// Dispatch generic theme click event
window.dispatchEvent(new CustomEvent('flasher:theme:click', { detail }))
// Dispatch theme-specific click event (e.g., flasher:theme:flasher:click)
const themeName = this.getThemeName(envelope)
if (themeName) {
window.dispatchEvent(new CustomEvent(`flasher:theme:${themeName}:click`, { detail }))
}
}
private getThemeName(envelope: Envelope): string {
const plugin = envelope.metadata?.plugin || ''
// Extract theme name from plugin (e.g., 'theme.flasher' -> 'flasher')
if (plugin.startsWith('theme.')) {
return plugin.replace('theme.', '')
}
// If it's the default 'flasher' plugin, return 'flasher'
if (plugin === 'flasher') {
return 'flasher'
}
return plugin
}
}
+2
View File
@@ -14,4 +14,6 @@ export default class FlasherPlugin extends AbstractPlugin {
private removeNotification;
private stringToHTML;
private escapeHtml;
private dispatchClickEvents;
private getThemeName;
}
+25
View File
@@ -227,6 +227,12 @@ class FlasherPlugin extends AbstractPlugin {
this.removeNotification(notification);
});
}
notification.addEventListener('click', (event) => {
if (event.target.closest('.fl-close')) {
return;
}
this.dispatchClickEvents(envelope);
});
if (options.timeout > 0) {
this.addTimer(notification, options);
}
@@ -331,6 +337,25 @@ class FlasherPlugin extends AbstractPlugin {
};
return str.replace(/[&<>"'`=/]/g, (char) => htmlEscapes[char] || char);
}
dispatchClickEvents(envelope) {
const detail = { envelope };
window.dispatchEvent(new CustomEvent('flasher:theme:click', { detail }));
const themeName = this.getThemeName(envelope);
if (themeName) {
window.dispatchEvent(new CustomEvent(`flasher:theme:${themeName}:click`, { detail }));
}
}
getThemeName(envelope) {
var _a;
const plugin = ((_a = envelope.metadata) === null || _a === void 0 ? void 0 : _a.plugin) || '';
if (plugin.startsWith('theme.')) {
return plugin.replace('theme.', '');
}
if (plugin === 'flasher') {
return 'flasher';
}
return plugin;
}
}
class Flasher extends AbstractPlugin {
+25
View File
@@ -233,6 +233,12 @@
this.removeNotification(notification);
});
}
notification.addEventListener('click', (event) => {
if (event.target.closest('.fl-close')) {
return;
}
this.dispatchClickEvents(envelope);
});
if (options.timeout > 0) {
this.addTimer(notification, options);
}
@@ -337,6 +343,25 @@
};
return str.replace(/[&<>"'`=/]/g, (char) => htmlEscapes[char] || char);
}
dispatchClickEvents(envelope) {
const detail = { envelope };
window.dispatchEvent(new CustomEvent('flasher:theme:click', { detail }));
const themeName = this.getThemeName(envelope);
if (themeName) {
window.dispatchEvent(new CustomEvent(`flasher:theme:${themeName}:click`, { detail }));
}
}
getThemeName(envelope) {
var _a;
const plugin = ((_a = envelope.metadata) === null || _a === void 0 ? void 0 : _a.plugin) || '';
if (plugin.startsWith('theme.')) {
return plugin.replace('theme.', '');
}
if (plugin === 'flasher') {
return 'flasher';
}
return plugin;
}
}
class Flasher extends AbstractPlugin {
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Flasher\Toastr\Laravel;
use Flasher\Laravel\Support\PluginServiceProvider;
use Flasher\Prime\EventDispatcher\EventDispatcherInterface;
use Flasher\Toastr\Prime\ToastrPlugin;
final class FlasherToastrServiceProvider extends PluginServiceProvider
@@ -13,4 +14,22 @@ final class FlasherToastrServiceProvider extends PluginServiceProvider
{
return new ToastrPlugin();
}
protected function afterBoot(): void
{
$this->registerLivewireListener();
}
private function registerLivewireListener(): void
{
if (!$this->app->bound('livewire')) {
return;
}
$this->app->extend('flasher.event_dispatcher', static function (EventDispatcherInterface $dispatcher) {
$dispatcher->addListener(new LivewireListener());
return $dispatcher;
});
}
}
+75
View File
@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace Flasher\Toastr\Laravel;
use Flasher\Prime\EventDispatcher\Event\ResponseEvent;
use Flasher\Prime\EventDispatcher\EventListener\EventListenerInterface;
final readonly class LivewireListener implements EventListenerInterface
{
public function __invoke(ResponseEvent $event): void
{
// Only process HTML responses
if ('html' !== $event->getPresenter()) {
return;
}
$response = $event->getResponse() ?: '';
if (!\is_string($response)) {
return;
}
// Avoid duplicate script injection
if (false === strripos($response, '<script type="text/javascript" class="flasher-js"')) {
return;
}
if (strripos($response, '<script type="text/javascript" class="flasher-toastr-livewire-js"')) {
return;
}
// Inject the Toastr-Livewire bridge JavaScript
$response .= <<<'JAVASCRIPT'
<script type="text/javascript" class="flasher-toastr-livewire-js">
(function() {
const events = ['flasher:toastr:show', 'flasher:toastr:click', 'flasher:toastr:close', 'flasher:toastr:hidden'];
events.forEach(function(eventName) {
window.addEventListener(eventName, function(event) {
if (typeof Livewire === 'undefined') {
return;
}
const { detail } = event;
const { envelope } = detail;
const context = envelope.context || {};
if (!context.livewire?.id) {
return;
}
const { livewire: { id: componentId } } = context;
const component = Livewire.all().find(c => c.id === componentId);
if (!component) {
return;
}
const livewireEventName = eventName.replace('flasher:', '').replace(':', ':');
Livewire.dispatchTo(component.name, livewireEventName, { payload: detail });
}, false);
});
})();
</script>
JAVASCRIPT;
$event->setResponse($response);
}
public function getSubscribedEvents(): string
{
return ResponseEvent::class;
}
}
+28 -1
View File
@@ -17,7 +17,28 @@ export default class ToastrPlugin extends AbstractPlugin {
try {
const { message, title, type, options } = envelope
const instance = toastr[type as ToastrType](message, title, options as ToastrOptions)
// Wrap callbacks to dispatch events
const mergedOptions = {
...options,
onShown: () => {
this.dispatchEvent('flasher:toastr:show', envelope);
(options as any)?.onShown?.()
},
onclick: () => {
this.dispatchEvent('flasher:toastr:click', envelope);
(options as any)?.onclick?.()
},
onCloseClick: () => {
this.dispatchEvent('flasher:toastr:close', envelope);
(options as any)?.onCloseClick?.()
},
onHidden: () => {
this.dispatchEvent('flasher:toastr:hidden', envelope);
(options as any)?.onHidden?.()
},
} as ToastrOptions
const instance = toastr[type as ToastrType](message, title, mergedOptions)
if (instance && instance.parent) {
try {
@@ -35,6 +56,12 @@ export default class ToastrPlugin extends AbstractPlugin {
})
}
private dispatchEvent(eventName: string, envelope: Envelope): void {
window.dispatchEvent(new CustomEvent(eventName, {
detail: { envelope },
}))
}
public renderOptions(options: Options): void {
if (!this.isDependencyAvailable()) {
return
+23 -1
View File
@@ -95,7 +95,24 @@ class ToastrPlugin extends AbstractPlugin {
envelopes.forEach((envelope) => {
try {
const { message, title, type, options } = envelope;
const instance = toastr[type](message, title, options);
const mergedOptions = Object.assign(Object.assign({}, options), { onShown: () => {
var _a;
this.dispatchEvent('flasher:toastr:show', envelope);
(_a = options === null || options === void 0 ? void 0 : options.onShown) === null || _a === void 0 ? void 0 : _a.call(options);
}, onclick: () => {
var _a;
this.dispatchEvent('flasher:toastr:click', envelope);
(_a = options === null || options === void 0 ? void 0 : options.onclick) === null || _a === void 0 ? void 0 : _a.call(options);
}, onCloseClick: () => {
var _a;
this.dispatchEvent('flasher:toastr:close', envelope);
(_a = options === null || options === void 0 ? void 0 : options.onCloseClick) === null || _a === void 0 ? void 0 : _a.call(options);
}, onHidden: () => {
var _a;
this.dispatchEvent('flasher:toastr:hidden', envelope);
(_a = options === null || options === void 0 ? void 0 : options.onHidden) === null || _a === void 0 ? void 0 : _a.call(options);
} });
const instance = toastr[type](message, title, mergedOptions);
if (instance && instance.parent) {
try {
const parent = instance.parent();
@@ -113,6 +130,11 @@ class ToastrPlugin extends AbstractPlugin {
}
});
}
dispatchEvent(eventName, envelope) {
window.dispatchEvent(new CustomEvent(eventName, {
detail: { envelope },
}));
}
renderOptions(options) {
if (!this.isDependencyAvailable()) {
return;
+23 -1
View File
@@ -98,7 +98,24 @@
envelopes.forEach((envelope) => {
try {
const { message, title, type, options } = envelope;
const instance = toastr[type](message, title, options);
const mergedOptions = Object.assign(Object.assign({}, options), { onShown: () => {
var _a;
this.dispatchEvent('flasher:toastr:show', envelope);
(_a = options === null || options === void 0 ? void 0 : options.onShown) === null || _a === void 0 ? void 0 : _a.call(options);
}, onclick: () => {
var _a;
this.dispatchEvent('flasher:toastr:click', envelope);
(_a = options === null || options === void 0 ? void 0 : options.onclick) === null || _a === void 0 ? void 0 : _a.call(options);
}, onCloseClick: () => {
var _a;
this.dispatchEvent('flasher:toastr:close', envelope);
(_a = options === null || options === void 0 ? void 0 : options.onCloseClick) === null || _a === void 0 ? void 0 : _a.call(options);
}, onHidden: () => {
var _a;
this.dispatchEvent('flasher:toastr:hidden', envelope);
(_a = options === null || options === void 0 ? void 0 : options.onHidden) === null || _a === void 0 ? void 0 : _a.call(options);
} });
const instance = toastr[type](message, title, mergedOptions);
if (instance && instance.parent) {
try {
const parent = instance.parent();
@@ -116,6 +133,11 @@
}
});
}
dispatchEvent(eventName, envelope) {
window.dispatchEvent(new CustomEvent(eventName, {
detail: { envelope },
}));
}
renderOptions(options) {
if (!this.isDependencyAvailable()) {
return;
+1 -1
View File
@@ -1 +1 @@
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("@flasher/flasher"),require("toastr")):"function"==typeof define&&define.amd?define(["@flasher/flasher","toastr"],t):(e="undefined"!=typeof globalThis?globalThis:e||self).toastr=t(e.flasher,e.toastr)}(this,(function(e,t){"use strict";class s{success(e,t,s){this.flash("success",e,t,s)}error(e,t,s){this.flash("error",e,t,s)}info(e,t,s){this.flash("info",e,t,s)}warning(e,t,s){this.flash("warning",e,t,s)}flash(e,t,s,r){let o,i,n,a={};if("object"==typeof e?(a=Object.assign({},e),o=a.type,i=a.message,n=a.title,delete a.type,delete a.message,delete a.title):"object"==typeof t?(a=Object.assign({},t),o=e,i=a.message,n=a.title,delete a.message,delete a.title):(o=e,i=t,null==s?(n=void 0,a=r||{}):"string"==typeof s?(n=s,a=r||{}):"object"==typeof s&&(a=Object.assign({},s),"title"in a?(n=a.title,delete a.title):n=void 0,r&&"object"==typeof r&&(a=Object.assign(Object.assign({},a),r)))),!o)throw new Error("Type is required for notifications");if(null==i)throw new Error("Message is required for notifications");null==n&&(n=o.charAt(0).toUpperCase()+o.slice(1));const l={type:o,message:i,title:n,options:a,metadata:{plugin:""}};this.renderOptions({}),this.renderEnvelopes([l])}}const r=new class extends s{renderEnvelopes(e){(null==e?void 0:e.length)&&this.isDependencyAvailable()&&e.forEach((e=>{try{const{message:s,title:r,type:o,options:i}=e,n=t[o](s,r,i);if(n&&n.parent)try{const e=n.parent();e&&"function"==typeof e.attr&&e.attr("data-turbo-temporary","")}catch(e){console.error("PHPFlasher Toastr: Error setting Turbo compatibility",e)}}catch(t){console.error("PHPFlasher Toastr: Error rendering notification",t,e)}}))}renderOptions(e){if(this.isDependencyAvailable())try{t.options=Object.assign({timeOut:e.timeOut||1e4,progressBar:e.progressBar||!0},e)}catch(e){console.error("PHPFlasher Toastr: Error applying options",e)}}isDependencyAvailable(){return!(!window.jQuery&&!window.$)||(console.error("PHPFlasher Toastr: jQuery is required but not loaded. Make sure jQuery is loaded before using Toastr."),!1)}};return e.addPlugin("toastr",r),r}));
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("@flasher/flasher"),require("toastr")):"function"==typeof define&&define.amd?define(["@flasher/flasher","toastr"],t):(e="undefined"!=typeof globalThis?globalThis:e||self).toastr=t(e.flasher,e.toastr)}(this,(function(e,t){"use strict";class s{success(e,t,s){this.flash("success",e,t,s)}error(e,t,s){this.flash("error",e,t,s)}info(e,t,s){this.flash("info",e,t,s)}warning(e,t,s){this.flash("warning",e,t,s)}flash(e,t,s,o){let i,r,n,l={};if("object"==typeof e?(l=Object.assign({},e),i=l.type,r=l.message,n=l.title,delete l.type,delete l.message,delete l.title):"object"==typeof t?(l=Object.assign({},t),i=e,r=l.message,n=l.title,delete l.message,delete l.title):(i=e,r=t,null==s?(n=void 0,l=o||{}):"string"==typeof s?(n=s,l=o||{}):"object"==typeof s&&(l=Object.assign({},s),"title"in l?(n=l.title,delete l.title):n=void 0,o&&"object"==typeof o&&(l=Object.assign(Object.assign({},l),o)))),!i)throw new Error("Type is required for notifications");if(null==r)throw new Error("Message is required for notifications");null==n&&(n=i.charAt(0).toUpperCase()+i.slice(1));const a={type:i,message:r,title:n,options:l,metadata:{plugin:""}};this.renderOptions({}),this.renderEnvelopes([a])}}const o=new class extends s{renderEnvelopes(e){(null==e?void 0:e.length)&&this.isDependencyAvailable()&&e.forEach((e=>{try{const{message:s,title:o,type:i,options:r}=e,n=Object.assign(Object.assign({},r),{onShown:()=>{var t;this.dispatchEvent("flasher:toastr:show",e),null===(t=null==r?void 0:r.onShown)||void 0===t||t.call(r)},onclick:()=>{var t;this.dispatchEvent("flasher:toastr:click",e),null===(t=null==r?void 0:r.onclick)||void 0===t||t.call(r)},onCloseClick:()=>{var t;this.dispatchEvent("flasher:toastr:close",e),null===(t=null==r?void 0:r.onCloseClick)||void 0===t||t.call(r)},onHidden:()=>{var t;this.dispatchEvent("flasher:toastr:hidden",e),null===(t=null==r?void 0:r.onHidden)||void 0===t||t.call(r)}}),l=t[i](s,o,n);if(l&&l.parent)try{const e=l.parent();e&&"function"==typeof e.attr&&e.attr("data-turbo-temporary","")}catch(e){console.error("PHPFlasher Toastr: Error setting Turbo compatibility",e)}}catch(t){console.error("PHPFlasher Toastr: Error rendering notification",t,e)}}))}dispatchEvent(e,t){window.dispatchEvent(new CustomEvent(e,{detail:{envelope:t}}))}renderOptions(e){if(this.isDependencyAvailable())try{t.options=Object.assign({timeOut:e.timeOut||1e4,progressBar:e.progressBar||!0},e)}catch(e){console.error("PHPFlasher Toastr: Error applying options",e)}}isDependencyAvailable(){return!(!window.jQuery&&!window.$)||(console.error("PHPFlasher Toastr: jQuery is required but not loaded. Make sure jQuery is loaded before using Toastr."),!1)}};return e.addPlugin("toastr",o),o}));
+1
View File
@@ -2,6 +2,7 @@ import { AbstractPlugin } from '@flasher/flasher/dist/plugin';
import type { Envelope, Options } from '@flasher/flasher/dist/types';
export default class ToastrPlugin extends AbstractPlugin {
renderEnvelopes(envelopes: Envelope[]): void;
private dispatchEvent;
renderOptions(options: Options): void;
private isDependencyAvailable;
}
+1 -1
View File
@@ -1 +1 @@
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("@flasher/flasher"),require("toastr")):"function"==typeof define&&define.amd?define(["@flasher/flasher","toastr"],t):(e="undefined"!=typeof globalThis?globalThis:e||self).toastr=t(e.flasher,e.toastr)}(this,(function(e,t){"use strict";class s{success(e,t,s){this.flash("success",e,t,s)}error(e,t,s){this.flash("error",e,t,s)}info(e,t,s){this.flash("info",e,t,s)}warning(e,t,s){this.flash("warning",e,t,s)}flash(e,t,s,r){let o,i,n,a={};if("object"==typeof e?(a=Object.assign({},e),o=a.type,i=a.message,n=a.title,delete a.type,delete a.message,delete a.title):"object"==typeof t?(a=Object.assign({},t),o=e,i=a.message,n=a.title,delete a.message,delete a.title):(o=e,i=t,null==s?(n=void 0,a=r||{}):"string"==typeof s?(n=s,a=r||{}):"object"==typeof s&&(a=Object.assign({},s),"title"in a?(n=a.title,delete a.title):n=void 0,r&&"object"==typeof r&&(a=Object.assign(Object.assign({},a),r)))),!o)throw new Error("Type is required for notifications");if(null==i)throw new Error("Message is required for notifications");null==n&&(n=o.charAt(0).toUpperCase()+o.slice(1));const l={type:o,message:i,title:n,options:a,metadata:{plugin:""}};this.renderOptions({}),this.renderEnvelopes([l])}}const r=new class extends s{renderEnvelopes(e){(null==e?void 0:e.length)&&this.isDependencyAvailable()&&e.forEach((e=>{try{const{message:s,title:r,type:o,options:i}=e,n=t[o](s,r,i);if(n&&n.parent)try{const e=n.parent();e&&"function"==typeof e.attr&&e.attr("data-turbo-temporary","")}catch(e){console.error("PHPFlasher Toastr: Error setting Turbo compatibility",e)}}catch(t){console.error("PHPFlasher Toastr: Error rendering notification",t,e)}}))}renderOptions(e){if(this.isDependencyAvailable())try{t.options=Object.assign({timeOut:e.timeOut||1e4,progressBar:e.progressBar||!0},e)}catch(e){console.error("PHPFlasher Toastr: Error applying options",e)}}isDependencyAvailable(){return!(!window.jQuery&&!window.$)||(console.error("PHPFlasher Toastr: jQuery is required but not loaded. Make sure jQuery is loaded before using Toastr."),!1)}};return e.addPlugin("toastr",r),r}));
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("@flasher/flasher"),require("toastr")):"function"==typeof define&&define.amd?define(["@flasher/flasher","toastr"],t):(e="undefined"!=typeof globalThis?globalThis:e||self).toastr=t(e.flasher,e.toastr)}(this,(function(e,t){"use strict";class s{success(e,t,s){this.flash("success",e,t,s)}error(e,t,s){this.flash("error",e,t,s)}info(e,t,s){this.flash("info",e,t,s)}warning(e,t,s){this.flash("warning",e,t,s)}flash(e,t,s,o){let i,r,n,l={};if("object"==typeof e?(l=Object.assign({},e),i=l.type,r=l.message,n=l.title,delete l.type,delete l.message,delete l.title):"object"==typeof t?(l=Object.assign({},t),i=e,r=l.message,n=l.title,delete l.message,delete l.title):(i=e,r=t,null==s?(n=void 0,l=o||{}):"string"==typeof s?(n=s,l=o||{}):"object"==typeof s&&(l=Object.assign({},s),"title"in l?(n=l.title,delete l.title):n=void 0,o&&"object"==typeof o&&(l=Object.assign(Object.assign({},l),o)))),!i)throw new Error("Type is required for notifications");if(null==r)throw new Error("Message is required for notifications");null==n&&(n=i.charAt(0).toUpperCase()+i.slice(1));const a={type:i,message:r,title:n,options:l,metadata:{plugin:""}};this.renderOptions({}),this.renderEnvelopes([a])}}const o=new class extends s{renderEnvelopes(e){(null==e?void 0:e.length)&&this.isDependencyAvailable()&&e.forEach((e=>{try{const{message:s,title:o,type:i,options:r}=e,n=Object.assign(Object.assign({},r),{onShown:()=>{var t;this.dispatchEvent("flasher:toastr:show",e),null===(t=null==r?void 0:r.onShown)||void 0===t||t.call(r)},onclick:()=>{var t;this.dispatchEvent("flasher:toastr:click",e),null===(t=null==r?void 0:r.onclick)||void 0===t||t.call(r)},onCloseClick:()=>{var t;this.dispatchEvent("flasher:toastr:close",e),null===(t=null==r?void 0:r.onCloseClick)||void 0===t||t.call(r)},onHidden:()=>{var t;this.dispatchEvent("flasher:toastr:hidden",e),null===(t=null==r?void 0:r.onHidden)||void 0===t||t.call(r)}}),l=t[i](s,o,n);if(l&&l.parent)try{const e=l.parent();e&&"function"==typeof e.attr&&e.attr("data-turbo-temporary","")}catch(e){console.error("PHPFlasher Toastr: Error setting Turbo compatibility",e)}}catch(t){console.error("PHPFlasher Toastr: Error rendering notification",t,e)}}))}dispatchEvent(e,t){window.dispatchEvent(new CustomEvent(e,{detail:{envelope:t}}))}renderOptions(e){if(this.isDependencyAvailable())try{t.options=Object.assign({timeOut:e.timeOut||1e4,progressBar:e.progressBar||!0},e)}catch(e){console.error("PHPFlasher Toastr: Error applying options",e)}}isDependencyAvailable(){return!(!window.jQuery&&!window.$)||(console.error("PHPFlasher Toastr: jQuery is required but not loaded. Make sure jQuery is loaded before using Toastr."),!1)}};return e.addPlugin("toastr",o),o}));
@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace Flasher\Tests\Laravel\EventListener;
use Flasher\Laravel\EventListener\ThemeLivewireListener;
use Flasher\Prime\EventDispatcher\Event\ResponseEvent;
use PHPUnit\Framework\TestCase;
final class ThemeLivewireListenerTest extends TestCase
{
private ThemeLivewireListener $listener;
protected function setUp(): void
{
parent::setUp();
$this->listener = new ThemeLivewireListener();
}
public function testGetSubscribedEvents(): void
{
$this->assertSame(ResponseEvent::class, $this->listener->getSubscribedEvents());
}
public function testInvokeSkipsNonHtmlPresenter(): void
{
$event = new ResponseEvent('', 'json');
($this->listener)($event);
$this->assertSame('', $event->getResponse());
}
public function testInvokeSkipsResponseWithoutFlasherScript(): void
{
$event = new ResponseEvent('<html><body>No flasher</body></html>', 'html');
($this->listener)($event);
$this->assertSame('<html><body>No flasher</body></html>', $event->getResponse());
}
public function testInvokeSkipsDuplicateInjection(): void
{
$response = '<script type="text/javascript" class="flasher-js"></script>';
$response .= '<script type="text/javascript" class="flasher-theme-livewire-js"></script>';
$event = new ResponseEvent($response, 'html');
($this->listener)($event);
$this->assertSame($response, $event->getResponse());
}
public function testInvokeInjectsLivewireScript(): void
{
$response = '<script type="text/javascript" class="flasher-js"></script>';
$event = new ResponseEvent($response, 'html');
($this->listener)($event);
$this->assertStringContainsString('flasher-theme-livewire-js', $event->getResponse());
$this->assertStringContainsString('flasher:theme:click', $event->getResponse());
$this->assertStringContainsString('theme:click', $event->getResponse());
$this->assertStringContainsString("theme:' + themeName + ':click", $event->getResponse());
}
}
@@ -53,6 +53,23 @@ final class FlasherNotyServiceProviderTest extends TestCase
$this->app->expects('singleton');
$this->app->expects('alias');
$this->app->expects('extend');
$this->app->expects('bound')->with('livewire')->andReturn(false);
$this->serviceProvider->register();
$this->serviceProvider->boot();
$this->addToAssertionCount(1);
}
public function testBootWithLivewire(): void
{
$this->app->expects()->make('config')->andReturns($configMock = \Mockery::mock(Repository::class));
$configMock->expects('get')->andReturns([]);
$configMock->expects('set');
$this->app->expects('singleton');
$this->app->expects('alias');
$this->app->expects('extend')->twice();
$this->app->expects('bound')->with('livewire')->andReturn(true);
$this->serviceProvider->register();
$this->serviceProvider->boot();
@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Flasher\Tests\Noty\Laravel;
use Flasher\Noty\Laravel\LivewireListener;
use Flasher\Prime\EventDispatcher\Event\ResponseEvent;
use PHPUnit\Framework\TestCase;
final class LivewireListenerTest extends TestCase
{
private LivewireListener $listener;
protected function setUp(): void
{
parent::setUp();
$this->listener = new LivewireListener();
}
public function testGetSubscribedEvents(): void
{
$this->assertSame(ResponseEvent::class, $this->listener->getSubscribedEvents());
}
public function testInvokeSkipsNonHtmlPresenter(): void
{
$event = new ResponseEvent('', 'json');
($this->listener)($event);
$this->assertSame('', $event->getResponse());
}
public function testInvokeSkipsResponseWithoutFlasherScript(): void
{
$event = new ResponseEvent('<html><body>No flasher</body></html>', 'html');
($this->listener)($event);
$this->assertSame('<html><body>No flasher</body></html>', $event->getResponse());
}
public function testInvokeSkipsDuplicateInjection(): void
{
$response = '<script type="text/javascript" class="flasher-js"></script>';
$response .= '<script type="text/javascript" class="flasher-noty-livewire-js"></script>';
$event = new ResponseEvent($response, 'html');
($this->listener)($event);
$this->assertSame($response, $event->getResponse());
}
public function testInvokeInjectsLivewireScript(): void
{
$response = '<script type="text/javascript" class="flasher-js"></script>';
$event = new ResponseEvent($response, 'html');
($this->listener)($event);
$this->assertStringContainsString('flasher-noty-livewire-js', $event->getResponse());
$this->assertStringContainsString('flasher:noty:show', $event->getResponse());
$this->assertStringContainsString('flasher:noty:click', $event->getResponse());
$this->assertStringContainsString('flasher:noty:close', $event->getResponse());
$this->assertStringContainsString('flasher:noty:hover', $event->getResponse());
}
}
@@ -53,6 +53,23 @@ final class FlasherNotyfServiceProviderTest extends TestCase
$this->app->expects('singleton');
$this->app->expects('alias');
$this->app->expects('extend');
$this->app->expects('bound')->with('livewire')->andReturn(false);
$this->serviceProvider->register();
$this->serviceProvider->boot();
$this->addToAssertionCount(1);
}
public function testBootWithLivewire(): void
{
$this->app->expects()->make('config')->andReturns($configMock = \Mockery::mock(Repository::class));
$configMock->expects('get')->andReturns([]);
$configMock->expects('set');
$this->app->expects('singleton');
$this->app->expects('alias');
$this->app->expects('extend')->twice();
$this->app->expects('bound')->with('livewire')->andReturn(true);
$this->serviceProvider->register();
$this->serviceProvider->boot();
@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace Flasher\Tests\Notyf\Laravel;
use Flasher\Notyf\Laravel\LivewireListener;
use Flasher\Prime\EventDispatcher\Event\ResponseEvent;
use PHPUnit\Framework\TestCase;
final class LivewireListenerTest extends TestCase
{
private LivewireListener $listener;
protected function setUp(): void
{
parent::setUp();
$this->listener = new LivewireListener();
}
public function testGetSubscribedEvents(): void
{
$this->assertSame(ResponseEvent::class, $this->listener->getSubscribedEvents());
}
public function testInvokeSkipsNonHtmlPresenter(): void
{
$event = new ResponseEvent('', 'json');
($this->listener)($event);
$this->assertSame('', $event->getResponse());
}
public function testInvokeSkipsResponseWithoutFlasherScript(): void
{
$event = new ResponseEvent('<html><body>No flasher</body></html>', 'html');
($this->listener)($event);
$this->assertSame('<html><body>No flasher</body></html>', $event->getResponse());
}
public function testInvokeSkipsDuplicateInjection(): void
{
$response = '<script type="text/javascript" class="flasher-js"></script>';
$response .= '<script type="text/javascript" class="flasher-notyf-livewire-js"></script>';
$event = new ResponseEvent($response, 'html');
($this->listener)($event);
$this->assertSame($response, $event->getResponse());
}
public function testInvokeInjectsLivewireScript(): void
{
$response = '<script type="text/javascript" class="flasher-js"></script>';
$event = new ResponseEvent($response, 'html');
($this->listener)($event);
$this->assertStringContainsString('flasher-notyf-livewire-js', $event->getResponse());
$this->assertStringContainsString('flasher:notyf:click', $event->getResponse());
$this->assertStringContainsString('flasher:notyf:dismiss', $event->getResponse());
}
}
@@ -53,6 +53,23 @@ final class FlasherToastrServiceProviderTest extends TestCase
$this->app->expects('singleton');
$this->app->expects('alias');
$this->app->expects('extend');
$this->app->expects('bound')->with('livewire')->andReturn(false);
$this->serviceProvider->register();
$this->serviceProvider->boot();
$this->addToAssertionCount(1);
}
public function testBootWithLivewire(): void
{
$this->app->expects()->make('config')->andReturns($configMock = \Mockery::mock(Repository::class));
$configMock->expects('get')->andReturns([]);
$configMock->expects('set');
$this->app->expects('singleton');
$this->app->expects('alias');
$this->app->expects('extend')->twice();
$this->app->expects('bound')->with('livewire')->andReturn(true);
$this->serviceProvider->register();
$this->serviceProvider->boot();
@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Flasher\Tests\Toastr\Laravel;
use Flasher\Prime\EventDispatcher\Event\ResponseEvent;
use Flasher\Toastr\Laravel\LivewireListener;
use PHPUnit\Framework\TestCase;
final class LivewireListenerTest extends TestCase
{
private LivewireListener $listener;
protected function setUp(): void
{
parent::setUp();
$this->listener = new LivewireListener();
}
public function testGetSubscribedEvents(): void
{
$this->assertSame(ResponseEvent::class, $this->listener->getSubscribedEvents());
}
public function testInvokeSkipsNonHtmlPresenter(): void
{
$event = new ResponseEvent('', 'json');
($this->listener)($event);
$this->assertSame('', $event->getResponse());
}
public function testInvokeSkipsResponseWithoutFlasherScript(): void
{
$event = new ResponseEvent('<html><body>No flasher</body></html>', 'html');
($this->listener)($event);
$this->assertSame('<html><body>No flasher</body></html>', $event->getResponse());
}
public function testInvokeSkipsDuplicateInjection(): void
{
$response = '<script type="text/javascript" class="flasher-js"></script>';
$response .= '<script type="text/javascript" class="flasher-toastr-livewire-js"></script>';
$event = new ResponseEvent($response, 'html');
($this->listener)($event);
$this->assertSame($response, $event->getResponse());
}
public function testInvokeInjectsLivewireScript(): void
{
$response = '<script type="text/javascript" class="flasher-js"></script>';
$event = new ResponseEvent($response, 'html');
($this->listener)($event);
$this->assertStringContainsString('flasher-toastr-livewire-js', $event->getResponse());
$this->assertStringContainsString('flasher:toastr:show', $event->getResponse());
$this->assertStringContainsString('flasher:toastr:click', $event->getResponse());
$this->assertStringContainsString('flasher:toastr:close', $event->getResponse());
$this->assertStringContainsString('flasher:toastr:hidden', $event->getResponse());
}
}
+95
View File
@@ -224,4 +224,99 @@ describe('NotyPlugin', () => {
})
})
})
describe('event dispatching', () => {
it('should set up event callbacks that dispatch custom events', () => {
plugin.renderEnvelopes([createEnvelope()])
// Verify callbacks are set up in options
const calledOptions = MockNoty.mock.calls[0][0]
expect(calledOptions.callbacks).toBeDefined()
expect(calledOptions.callbacks.onShow).toBeInstanceOf(Function)
expect(calledOptions.callbacks.onClick).toBeInstanceOf(Function)
expect(calledOptions.callbacks.onClose).toBeInstanceOf(Function)
expect(calledOptions.callbacks.onHover).toBeInstanceOf(Function)
})
it('should dispatch flasher:noty:show event when onShow callback is called', () => {
const eventHandler = vi.fn()
window.addEventListener('flasher:noty:show', eventHandler)
plugin.renderEnvelopes([createEnvelope({ message: 'Test' })])
const calledOptions = MockNoty.mock.calls[0][0]
calledOptions.callbacks.onShow()
expect(eventHandler).toHaveBeenCalledWith(expect.objectContaining({
detail: expect.objectContaining({
envelope: expect.objectContaining({ message: 'Test' }),
}),
}))
window.removeEventListener('flasher:noty:show', eventHandler)
})
it('should dispatch flasher:noty:click event when onClick callback is called', () => {
const eventHandler = vi.fn()
window.addEventListener('flasher:noty:click', eventHandler)
plugin.renderEnvelopes([createEnvelope({ message: 'Click test' })])
const calledOptions = MockNoty.mock.calls[0][0]
calledOptions.callbacks.onClick()
expect(eventHandler).toHaveBeenCalledWith(expect.objectContaining({
detail: expect.objectContaining({
envelope: expect.objectContaining({ message: 'Click test' }),
}),
}))
window.removeEventListener('flasher:noty:click', eventHandler)
})
it('should dispatch flasher:noty:close event when onClose callback is called', () => {
const eventHandler = vi.fn()
window.addEventListener('flasher:noty:close', eventHandler)
plugin.renderEnvelopes([createEnvelope()])
const calledOptions = MockNoty.mock.calls[0][0]
calledOptions.callbacks.onClose()
expect(eventHandler).toHaveBeenCalled()
window.removeEventListener('flasher:noty:close', eventHandler)
})
it('should dispatch flasher:noty:hover event when onHover callback is called', () => {
const eventHandler = vi.fn()
window.addEventListener('flasher:noty:hover', eventHandler)
plugin.renderEnvelopes([createEnvelope()])
const calledOptions = MockNoty.mock.calls[0][0]
calledOptions.callbacks.onHover()
expect(eventHandler).toHaveBeenCalled()
window.removeEventListener('flasher:noty:hover', eventHandler)
})
it('should call original callbacks if provided', () => {
const originalOnClick = vi.fn()
plugin.renderEnvelopes([createEnvelope({
options: {
callbacks: {
onClick: originalOnClick,
},
},
})])
const calledOptions = MockNoty.mock.calls[0][0]
calledOptions.callbacks.onClick()
expect(originalOnClick).toHaveBeenCalled()
})
})
})
+91 -1
View File
@@ -81,7 +81,7 @@ describe('ToastrPlugin', () => {
expect(mockToastr.success).toHaveBeenCalledWith(
'Hello World',
'Greeting',
{ timeOut: 5000 },
expect.objectContaining({ timeOut: 5000 }),
)
})
@@ -218,4 +218,94 @@ describe('ToastrPlugin', () => {
expect(mockToastr.warning).toHaveBeenCalled()
})
})
describe('event dispatching', () => {
it('should set up event callbacks that dispatch custom events', () => {
plugin.renderEnvelopes([createEnvelope()])
// Verify options passed to toastr include event callbacks
const calledOptions = mockToastr.success.mock.calls[0][2]
expect(calledOptions.onShown).toBeInstanceOf(Function)
expect(calledOptions.onclick).toBeInstanceOf(Function)
expect(calledOptions.onCloseClick).toBeInstanceOf(Function)
expect(calledOptions.onHidden).toBeInstanceOf(Function)
})
it('should dispatch flasher:toastr:show event when onShown callback is called', () => {
const eventHandler = vi.fn()
window.addEventListener('flasher:toastr:show', eventHandler)
plugin.renderEnvelopes([createEnvelope({ message: 'Show test' })])
const calledOptions = mockToastr.success.mock.calls[0][2]
calledOptions.onShown()
expect(eventHandler).toHaveBeenCalledWith(expect.objectContaining({
detail: expect.objectContaining({
envelope: expect.objectContaining({ message: 'Show test' }),
}),
}))
window.removeEventListener('flasher:toastr:show', eventHandler)
})
it('should dispatch flasher:toastr:click event when onclick callback is called', () => {
const eventHandler = vi.fn()
window.addEventListener('flasher:toastr:click', eventHandler)
plugin.renderEnvelopes([createEnvelope({ message: 'Click test' })])
const calledOptions = mockToastr.success.mock.calls[0][2]
calledOptions.onclick()
expect(eventHandler).toHaveBeenCalledWith(expect.objectContaining({
detail: expect.objectContaining({
envelope: expect.objectContaining({ message: 'Click test' }),
}),
}))
window.removeEventListener('flasher:toastr:click', eventHandler)
})
it('should dispatch flasher:toastr:close event when onCloseClick callback is called', () => {
const eventHandler = vi.fn()
window.addEventListener('flasher:toastr:close', eventHandler)
plugin.renderEnvelopes([createEnvelope()])
const calledOptions = mockToastr.success.mock.calls[0][2]
calledOptions.onCloseClick()
expect(eventHandler).toHaveBeenCalled()
window.removeEventListener('flasher:toastr:close', eventHandler)
})
it('should dispatch flasher:toastr:hidden event when onHidden callback is called', () => {
const eventHandler = vi.fn()
window.addEventListener('flasher:toastr:hidden', eventHandler)
plugin.renderEnvelopes([createEnvelope()])
const calledOptions = mockToastr.success.mock.calls[0][2]
calledOptions.onHidden()
expect(eventHandler).toHaveBeenCalled()
window.removeEventListener('flasher:toastr:hidden', eventHandler)
})
it('should call original callbacks if provided', () => {
const originalOnClick = vi.fn()
plugin.renderEnvelopes([createEnvelope({
options: { onclick: originalOnClick },
})])
const calledOptions = mockToastr.success.mock.calls[0][2]
calledOptions.onclick()
expect(originalOnClick).toHaveBeenCalled()
})
})
})
+95
View File
@@ -675,4 +675,99 @@ describe('FlasherPlugin', () => {
})
})
})
describe('click event dispatching', () => {
it('should dispatch flasher:theme:click event when notification is clicked', () => {
const eventHandler = vi.fn()
window.addEventListener('flasher:theme:click', eventHandler)
plugin.renderEnvelopes([createEnvelope({ message: 'Click me' })])
const notification = document.querySelector('.fl-notification') as HTMLElement
expect(notification).toBeTruthy()
notification.click()
expect(eventHandler).toHaveBeenCalledWith(expect.objectContaining({
detail: expect.objectContaining({
envelope: expect.objectContaining({ message: 'Click me' }),
}),
}))
window.removeEventListener('flasher:theme:click', eventHandler)
})
it('should dispatch theme-specific click event (flasher:theme:{name}:click)', () => {
const eventHandler = vi.fn()
window.addEventListener('flasher:theme:test:click', eventHandler)
plugin.renderEnvelopes([createEnvelope({
message: 'Theme specific',
metadata: { plugin: 'theme.test' },
})])
const notification = document.querySelector('.fl-notification') as HTMLElement
notification.click()
expect(eventHandler).toHaveBeenCalledWith(expect.objectContaining({
detail: expect.objectContaining({
envelope: expect.objectContaining({ message: 'Theme specific' }),
}),
}))
window.removeEventListener('flasher:theme:test:click', eventHandler)
})
it('should dispatch both generic and specific events on click', () => {
const genericHandler = vi.fn()
const specificHandler = vi.fn()
window.addEventListener('flasher:theme:click', genericHandler)
window.addEventListener('flasher:theme:test:click', specificHandler)
plugin.renderEnvelopes([createEnvelope({
metadata: { plugin: 'theme.test' },
})])
const notification = document.querySelector('.fl-notification') as HTMLElement
notification.click()
expect(genericHandler).toHaveBeenCalled()
expect(specificHandler).toHaveBeenCalled()
window.removeEventListener('flasher:theme:click', genericHandler)
window.removeEventListener('flasher:theme:test:click', specificHandler)
})
it('should not dispatch click event when close button is clicked', () => {
const eventHandler = vi.fn()
window.addEventListener('flasher:theme:click', eventHandler)
plugin.renderEnvelopes([createEnvelope()])
const closeButton = document.querySelector('.fl-close') as HTMLElement
expect(closeButton).toBeTruthy()
closeButton.click()
expect(eventHandler).not.toHaveBeenCalled()
window.removeEventListener('flasher:theme:click', eventHandler)
})
it('should handle flasher plugin alias correctly', () => {
const eventHandler = vi.fn()
window.addEventListener('flasher:theme:flasher:click', eventHandler)
plugin.renderEnvelopes([createEnvelope({
metadata: { plugin: 'flasher' },
})])
const notification = document.querySelector('.fl-notification') as HTMLElement
notification.click()
expect(eventHandler).toHaveBeenCalled()
window.removeEventListener('flasher:theme:flasher:click', eventHandler)
})
})
})