From 7d6e9b46b803a691c7aa5291e82b908f5d4c69d4 Mon Sep 17 00:00:00 2001 From: Younes ENNAJI Date: Sun, 1 Mar 2026 21:05:10 +0000 Subject: [PATCH] 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). --- .../EventListener/ThemeLivewireListener.php | 80 ++++++++++++++++ src/Laravel/FlasherServiceProvider.php | 13 +++ .../Laravel/FlasherNotyServiceProvider.php | 19 ++++ src/Noty/Laravel/LivewireListener.php | 75 +++++++++++++++ src/Noty/Prime/Resources/assets/noty.ts | 34 +++++++ .../Prime/Resources/dist/flasher-noty.esm.js | 29 ++++++ src/Noty/Prime/Resources/dist/flasher-noty.js | 29 ++++++ .../Prime/Resources/dist/flasher-noty.min.js | 2 +- src/Noty/Prime/Resources/dist/noty.d.ts | 1 + .../Resources/public/flasher-noty.min.js | 2 +- .../Laravel/FlasherNotyfServiceProvider.php | 19 ++++ src/Notyf/Laravel/LivewireListener.php | 75 +++++++++++++++ src/Notyf/Prime/Resources/assets/notyf.ts | 37 +++++++- .../Prime/Resources/dist/flasher-notyf.esm.js | 26 ++++- .../Prime/Resources/dist/flasher-notyf.js | 26 ++++- .../Prime/Resources/dist/flasher-notyf.min.js | 2 +- src/Notyf/Prime/Resources/dist/notyf.d.ts | 2 + .../Resources/public/flasher-notyf.min.js | 2 +- src/Prime/Resources/assets/flasher-plugin.ts | 38 ++++++++ src/Prime/Resources/dist/flasher-plugin.d.ts | 2 + src/Prime/Resources/dist/flasher.esm.js | 25 +++++ src/Prime/Resources/dist/flasher.js | 25 +++++ src/Prime/Resources/dist/flasher.min.js | 2 +- src/Prime/Resources/public/flasher.min.js | 2 +- .../Laravel/FlasherToastrServiceProvider.php | 19 ++++ src/Toastr/Laravel/LivewireListener.php | 75 +++++++++++++++ src/Toastr/Prime/Resources/assets/toastr.ts | 29 +++++- .../Resources/dist/flasher-toastr.esm.js | 24 ++++- .../Prime/Resources/dist/flasher-toastr.js | 24 ++++- .../Resources/dist/flasher-toastr.min.js | 2 +- src/Toastr/Prime/Resources/dist/toastr.d.ts | 1 + .../Resources/public/flasher-toastr.min.js | 2 +- .../ThemeLivewireListenerTest.php | 70 ++++++++++++++ .../FlasherNotyServiceProviderTest.php | 17 ++++ tests/Noty/Laravel/LivewireListenerTest.php | 71 ++++++++++++++ .../FlasherNotyfServiceProviderTest.php | 17 ++++ tests/Notyf/Laravel/LivewireListenerTest.php | 69 ++++++++++++++ .../FlasherToastrServiceProviderTest.php | 17 ++++ tests/Toastr/Laravel/LivewireListenerTest.php | 71 ++++++++++++++ tests/adapters/noty.test.ts | 95 +++++++++++++++++++ tests/adapters/toastr.test.ts | 92 +++++++++++++++++- tests/flasher-plugin.test.ts | 95 +++++++++++++++++++ 42 files changed, 1342 insertions(+), 15 deletions(-) create mode 100644 src/Laravel/EventListener/ThemeLivewireListener.php create mode 100644 src/Noty/Laravel/LivewireListener.php create mode 100644 src/Notyf/Laravel/LivewireListener.php create mode 100644 src/Toastr/Laravel/LivewireListener.php create mode 100644 tests/Laravel/EventListener/ThemeLivewireListenerTest.php create mode 100644 tests/Noty/Laravel/LivewireListenerTest.php create mode 100644 tests/Notyf/Laravel/LivewireListenerTest.php create mode 100644 tests/Toastr/Laravel/LivewireListenerTest.php diff --git a/src/Laravel/EventListener/ThemeLivewireListener.php b/src/Laravel/EventListener/ThemeLivewireListener.php new file mode 100644 index 00000000..e97eb4c9 --- /dev/null +++ b/src/Laravel/EventListener/ThemeLivewireListener.php @@ -0,0 +1,80 @@ +getPresenter()) { + return; + } + + $response = $event->getResponse() ?: ''; + if (!\is_string($response)) { + return; + } + + // Avoid duplicate script injection + if (false === strripos($response, ' +JAVASCRIPT; + + $event->setResponse($response); + } + + public function getSubscribedEvents(): string + { + return ResponseEvent::class; + } +} diff --git a/src/Laravel/FlasherServiceProvider.php b/src/Laravel/FlasherServiceProvider.php index aa2d6cb8..411398c2 100644 --- a/src/Laravel/FlasherServiceProvider.php +++ b/src/Laravel/FlasherServiceProvider.php @@ -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; + }); } } diff --git a/src/Noty/Laravel/FlasherNotyServiceProvider.php b/src/Noty/Laravel/FlasherNotyServiceProvider.php index 4322dcae..4722cdab 100644 --- a/src/Noty/Laravel/FlasherNotyServiceProvider.php +++ b/src/Noty/Laravel/FlasherNotyServiceProvider.php @@ -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; + }); + } } diff --git a/src/Noty/Laravel/LivewireListener.php b/src/Noty/Laravel/LivewireListener.php new file mode 100644 index 00000000..9e97d584 --- /dev/null +++ b/src/Noty/Laravel/LivewireListener.php @@ -0,0 +1,75 @@ +getPresenter()) { + return; + } + + $response = $event->getResponse() ?: ''; + if (!\is_string($response)) { + return; + } + + // Avoid duplicate script injection + if (false === strripos($response, ' +JAVASCRIPT; + + $event->setResponse($response); + } + + public function getSubscribedEvents(): string + { + return ResponseEvent::class; + } +} diff --git a/src/Noty/Prime/Resources/assets/noty.ts b/src/Noty/Prime/Resources/assets/noty.ts index 73ece312..8fc06a17 100644 --- a/src/Noty/Prime/Resources/assets/noty.ts +++ b/src/Noty/Prime/Resources/assets/noty.ts @@ -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 diff --git a/src/Noty/Prime/Resources/dist/flasher-noty.esm.js b/src/Noty/Prime/Resources/dist/flasher-noty.esm.js index 23c3e346..0fdd39b5 100644 --- a/src/Noty/Prime/Resources/dist/flasher-noty.esm.js +++ b/src/Noty/Prime/Resources/dist/flasher-noty.esm.js @@ -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; diff --git a/src/Noty/Prime/Resources/dist/flasher-noty.js b/src/Noty/Prime/Resources/dist/flasher-noty.js index fa81c382..7e774494 100644 --- a/src/Noty/Prime/Resources/dist/flasher-noty.js +++ b/src/Noty/Prime/Resources/dist/flasher-noty.js @@ -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; diff --git a/src/Noty/Prime/Resources/dist/flasher-noty.min.js b/src/Noty/Prime/Resources/dist/flasher-noty.min.js index 496f65df..be4c28ee 100644 --- a/src/Noty/Prime/Resources/dist/flasher-noty.min.js +++ b/src/Noty/Prime/Resources/dist/flasher-noty.min.js @@ -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})); diff --git a/src/Noty/Prime/Resources/dist/noty.d.ts b/src/Noty/Prime/Resources/dist/noty.d.ts index 2c6b19e4..7ca6adf4 100644 --- a/src/Noty/Prime/Resources/dist/noty.d.ts +++ b/src/Noty/Prime/Resources/dist/noty.d.ts @@ -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; } diff --git a/src/Noty/Prime/Resources/public/flasher-noty.min.js b/src/Noty/Prime/Resources/public/flasher-noty.min.js index 496f65df..be4c28ee 100644 --- a/src/Noty/Prime/Resources/public/flasher-noty.min.js +++ b/src/Noty/Prime/Resources/public/flasher-noty.min.js @@ -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})); diff --git a/src/Notyf/Laravel/FlasherNotyfServiceProvider.php b/src/Notyf/Laravel/FlasherNotyfServiceProvider.php index d43ca3a4..0f93b817 100644 --- a/src/Notyf/Laravel/FlasherNotyfServiceProvider.php +++ b/src/Notyf/Laravel/FlasherNotyfServiceProvider.php @@ -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; + }); + } } diff --git a/src/Notyf/Laravel/LivewireListener.php b/src/Notyf/Laravel/LivewireListener.php new file mode 100644 index 00000000..4710930d --- /dev/null +++ b/src/Notyf/Laravel/LivewireListener.php @@ -0,0 +1,75 @@ +getPresenter()) { + return; + } + + $response = $event->getResponse() ?: ''; + if (!\is_string($response)) { + return; + } + + // Avoid duplicate script injection + if (false === strripos($response, ' +JAVASCRIPT; + + $event->setResponse($response); + } + + public function getSubscribedEvents(): string + { + return ResponseEvent::class; + } +} diff --git a/src/Notyf/Prime/Resources/assets/notyf.ts b/src/Notyf/Prime/Resources/assets/notyf.ts index 373d2482..cf998cae 100644 --- a/src/Notyf/Prime/Resources/assets/notyf.ts +++ b/src/Notyf/Prime/Resources/assets/notyf.ts @@ -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 = {}): void { + window.dispatchEvent(new CustomEvent(eventName, { + detail: { envelope, ...extra }, + })) + } } diff --git a/src/Notyf/Prime/Resources/dist/flasher-notyf.esm.js b/src/Notyf/Prime/Resources/dist/flasher-notyf.esm.js index bf7e61d1..d53d00c8 100644 --- a/src/Notyf/Prime/Resources/dist/flasher-notyf.esm.js +++ b/src/Notyf/Prime/Resources/dist/flasher-notyf.esm.js @@ -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(); diff --git a/src/Notyf/Prime/Resources/dist/flasher-notyf.js b/src/Notyf/Prime/Resources/dist/flasher-notyf.js index 9d5f3109..3cd0bf50 100644 --- a/src/Notyf/Prime/Resources/dist/flasher-notyf.js +++ b/src/Notyf/Prime/Resources/dist/flasher-notyf.js @@ -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(); diff --git a/src/Notyf/Prime/Resources/dist/flasher-notyf.min.js b/src/Notyf/Prime/Resources/dist/flasher-notyf.min.js index 8528c86d..33feb4cb 100644 --- a/src/Notyf/Prime/Resources/dist/flasher-notyf.min.js +++ b/src/Notyf/Prime/Resources/dist/flasher-notyf.min.js @@ -1 +1 @@ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("@flasher/flasher")):"function"==typeof define&&define.amd?define(["@flasher/flasher"],e):(t="undefined"!=typeof globalThis?globalThis:t||self).notyf=e(t.flasher)}(this,(function(t){"use strict";class e{success(t,e,i){this.flash("success",t,e,i)}error(t,e,i){this.flash("error",t,e,i)}info(t,e,i){this.flash("info",t,e,i)}warning(t,e,i){this.flash("warning",t,e,i)}flash(t,e,i,n){let o,s,a,r={};if("object"==typeof t?(r=Object.assign({},t),o=r.type,s=r.message,a=r.title,delete r.type,delete r.message,delete r.title):"object"==typeof e?(r=Object.assign({},e),o=t,s=r.message,a=r.title,delete r.message,delete r.title):(o=t,s=e,null==i?(a=void 0,r=n||{}):"string"==typeof i?(a=i,r=n||{}):"object"==typeof i&&(r=Object.assign({},i),"title"in r?(a=r.title,delete r.title):a=void 0,n&&"object"==typeof n&&(r=Object.assign(Object.assign({},r),n)))),!o)throw new Error("Type is required for notifications");if(null==s)throw new Error("Message is required for notifications");null==a&&(a=o.charAt(0).toUpperCase()+o.slice(1));const c={type:o,message:s,title:a,options:r,metadata:{plugin:""}};this.renderOptions({}),this.renderEnvelopes([c])}}var i,n=function(){return n=Object.assign||function(t){for(var e,i=1,n=arguments.length;i{var e;try{const i=Object.assign(Object.assign({},t),t.options);null===(e=this.notyf)||void 0===e||e.open(i)}catch(e){console.error("PHPFlasher Notyf: Error rendering notification",e,t)}}));try{if(this.notyf){const t=this.notyf.view,e=t.container,i=t.a11yContainer;e&&e.dataset&&(e.dataset.turboTemporary=""),i&&i.dataset&&(i.dataset.turboTemporary="")}}catch(t){console.error("PHPFlasher Notyf: Error setting Turbo compatibility",t)}}renderOptions(t){if(!t)return;const e=Object.assign({duration:t.duration||1e4},t);e.types=e.types||[],this.addTypeIfNotExists(e.types,{type:"info",className:"notyf__toast--info",background:"#5784E5",icon:{className:"notyf__icon--info",tagName:"i"}}),this.addTypeIfNotExists(e.types,{type:"warning",className:"notyf__toast--warning",background:"#E3A008",icon:{className:"notyf__icon--warning",tagName:"i"}}),this.notyf=this.notyf||new p(e)}initializeNotyf(){this.notyf||this.renderOptions({duration:1e4,position:{x:"right",y:"top"},dismissible:!0})}addTypeIfNotExists(t,e){t.some((t=>t.type===e.type))||t.push(e)}};return t.addPlugin("notyf",l),l})); +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("@flasher/flasher")):"function"==typeof define&&define.amd?define(["@flasher/flasher"],e):(t="undefined"!=typeof globalThis?globalThis:t||self).notyf=e(t.flasher)}(this,(function(t){"use strict";class e{success(t,e,i){this.flash("success",t,e,i)}error(t,e,i){this.flash("error",t,e,i)}info(t,e,i){this.flash("info",t,e,i)}warning(t,e,i){this.flash("warning",t,e,i)}flash(t,e,i,n){let o,s,a,r={};if("object"==typeof t?(r=Object.assign({},t),o=r.type,s=r.message,a=r.title,delete r.type,delete r.message,delete r.title):"object"==typeof e?(r=Object.assign({},e),o=t,s=r.message,a=r.title,delete r.message,delete r.title):(o=t,s=e,null==i?(a=void 0,r=n||{}):"string"==typeof i?(a=i,r=n||{}):"object"==typeof i&&(r=Object.assign({},i),"title"in r?(a=r.title,delete r.title):a=void 0,n&&"object"==typeof n&&(r=Object.assign(Object.assign({},r),n)))),!o)throw new Error("Type is required for notifications");if(null==s)throw new Error("Message is required for notifications");null==a&&(a=o.charAt(0).toUpperCase()+o.slice(1));const c={type:o,message:s,title:a,options:r,metadata:{plugin:""}};this.renderOptions({}),this.renderEnvelopes([c])}}var i,n=function(){return n=Object.assign||function(t){for(var e,i=1,n=arguments.length;i{var e;try{const i=Object.assign(Object.assign({},t),t.options),n=null===(e=this.notyf)||void 0===e?void 0:e.open(i);n&&this.attachEventListeners(n,t)}catch(e){console.error("PHPFlasher Notyf: Error rendering notification",e,t)}}));try{if(this.notyf){const t=this.notyf.view,e=t.container,i=t.a11yContainer;e&&e.dataset&&(e.dataset.turboTemporary=""),i&&i.dataset&&(i.dataset.turboTemporary="")}}catch(t){console.error("PHPFlasher Notyf: Error setting Turbo compatibility",t)}}renderOptions(t){if(!t)return;const e=Object.assign({duration:t.duration||1e4},t);e.types=e.types||[],this.addTypeIfNotExists(e.types,{type:"info",className:"notyf__toast--info",background:"#5784E5",icon:{className:"notyf__icon--info",tagName:"i"}}),this.addTypeIfNotExists(e.types,{type:"warning",className:"notyf__toast--warning",background:"#E3A008",icon:{className:"notyf__icon--warning",tagName:"i"}}),this.notyf=this.notyf||new p(e)}initializeNotyf(){this.notyf||this.renderOptions({duration:1e4,position:{x:"right",y:"top"},dismissible:!0})}addTypeIfNotExists(t,e){t.some((t=>t.type===e.type))||t.push(e)}attachEventListeners(t,e){if(!this.notyf)return;const i=this.notyf;i.on("click",(({target:i,event:n})=>{i===t&&this.dispatchEvent("flasher:notyf:click",e,{event:n})})),i.on("dismiss",(({target:i,event:n})=>{i===t&&this.dispatchEvent("flasher:notyf:dismiss",e,{event:n})}))}dispatchEvent(t,e,i={}){window.dispatchEvent(new CustomEvent(t,{detail:Object.assign({envelope:e},i)}))}};return t.addPlugin("notyf",f),f})); diff --git a/src/Notyf/Prime/Resources/dist/notyf.d.ts b/src/Notyf/Prime/Resources/dist/notyf.d.ts index 72d07d07..728cbad5 100644 --- a/src/Notyf/Prime/Resources/dist/notyf.d.ts +++ b/src/Notyf/Prime/Resources/dist/notyf.d.ts @@ -7,4 +7,6 @@ export default class NotyfPlugin extends AbstractPlugin { renderOptions(options: Options): void; private initializeNotyf; private addTypeIfNotExists; + private attachEventListeners; + private dispatchEvent; } diff --git a/src/Notyf/Prime/Resources/public/flasher-notyf.min.js b/src/Notyf/Prime/Resources/public/flasher-notyf.min.js index 8528c86d..33feb4cb 100644 --- a/src/Notyf/Prime/Resources/public/flasher-notyf.min.js +++ b/src/Notyf/Prime/Resources/public/flasher-notyf.min.js @@ -1 +1 @@ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("@flasher/flasher")):"function"==typeof define&&define.amd?define(["@flasher/flasher"],e):(t="undefined"!=typeof globalThis?globalThis:t||self).notyf=e(t.flasher)}(this,(function(t){"use strict";class e{success(t,e,i){this.flash("success",t,e,i)}error(t,e,i){this.flash("error",t,e,i)}info(t,e,i){this.flash("info",t,e,i)}warning(t,e,i){this.flash("warning",t,e,i)}flash(t,e,i,n){let o,s,a,r={};if("object"==typeof t?(r=Object.assign({},t),o=r.type,s=r.message,a=r.title,delete r.type,delete r.message,delete r.title):"object"==typeof e?(r=Object.assign({},e),o=t,s=r.message,a=r.title,delete r.message,delete r.title):(o=t,s=e,null==i?(a=void 0,r=n||{}):"string"==typeof i?(a=i,r=n||{}):"object"==typeof i&&(r=Object.assign({},i),"title"in r?(a=r.title,delete r.title):a=void 0,n&&"object"==typeof n&&(r=Object.assign(Object.assign({},r),n)))),!o)throw new Error("Type is required for notifications");if(null==s)throw new Error("Message is required for notifications");null==a&&(a=o.charAt(0).toUpperCase()+o.slice(1));const c={type:o,message:s,title:a,options:r,metadata:{plugin:""}};this.renderOptions({}),this.renderEnvelopes([c])}}var i,n=function(){return n=Object.assign||function(t){for(var e,i=1,n=arguments.length;i{var e;try{const i=Object.assign(Object.assign({},t),t.options);null===(e=this.notyf)||void 0===e||e.open(i)}catch(e){console.error("PHPFlasher Notyf: Error rendering notification",e,t)}}));try{if(this.notyf){const t=this.notyf.view,e=t.container,i=t.a11yContainer;e&&e.dataset&&(e.dataset.turboTemporary=""),i&&i.dataset&&(i.dataset.turboTemporary="")}}catch(t){console.error("PHPFlasher Notyf: Error setting Turbo compatibility",t)}}renderOptions(t){if(!t)return;const e=Object.assign({duration:t.duration||1e4},t);e.types=e.types||[],this.addTypeIfNotExists(e.types,{type:"info",className:"notyf__toast--info",background:"#5784E5",icon:{className:"notyf__icon--info",tagName:"i"}}),this.addTypeIfNotExists(e.types,{type:"warning",className:"notyf__toast--warning",background:"#E3A008",icon:{className:"notyf__icon--warning",tagName:"i"}}),this.notyf=this.notyf||new p(e)}initializeNotyf(){this.notyf||this.renderOptions({duration:1e4,position:{x:"right",y:"top"},dismissible:!0})}addTypeIfNotExists(t,e){t.some((t=>t.type===e.type))||t.push(e)}};return t.addPlugin("notyf",l),l})); +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("@flasher/flasher")):"function"==typeof define&&define.amd?define(["@flasher/flasher"],e):(t="undefined"!=typeof globalThis?globalThis:t||self).notyf=e(t.flasher)}(this,(function(t){"use strict";class e{success(t,e,i){this.flash("success",t,e,i)}error(t,e,i){this.flash("error",t,e,i)}info(t,e,i){this.flash("info",t,e,i)}warning(t,e,i){this.flash("warning",t,e,i)}flash(t,e,i,n){let o,s,a,r={};if("object"==typeof t?(r=Object.assign({},t),o=r.type,s=r.message,a=r.title,delete r.type,delete r.message,delete r.title):"object"==typeof e?(r=Object.assign({},e),o=t,s=r.message,a=r.title,delete r.message,delete r.title):(o=t,s=e,null==i?(a=void 0,r=n||{}):"string"==typeof i?(a=i,r=n||{}):"object"==typeof i&&(r=Object.assign({},i),"title"in r?(a=r.title,delete r.title):a=void 0,n&&"object"==typeof n&&(r=Object.assign(Object.assign({},r),n)))),!o)throw new Error("Type is required for notifications");if(null==s)throw new Error("Message is required for notifications");null==a&&(a=o.charAt(0).toUpperCase()+o.slice(1));const c={type:o,message:s,title:a,options:r,metadata:{plugin:""}};this.renderOptions({}),this.renderEnvelopes([c])}}var i,n=function(){return n=Object.assign||function(t){for(var e,i=1,n=arguments.length;i{var e;try{const i=Object.assign(Object.assign({},t),t.options),n=null===(e=this.notyf)||void 0===e?void 0:e.open(i);n&&this.attachEventListeners(n,t)}catch(e){console.error("PHPFlasher Notyf: Error rendering notification",e,t)}}));try{if(this.notyf){const t=this.notyf.view,e=t.container,i=t.a11yContainer;e&&e.dataset&&(e.dataset.turboTemporary=""),i&&i.dataset&&(i.dataset.turboTemporary="")}}catch(t){console.error("PHPFlasher Notyf: Error setting Turbo compatibility",t)}}renderOptions(t){if(!t)return;const e=Object.assign({duration:t.duration||1e4},t);e.types=e.types||[],this.addTypeIfNotExists(e.types,{type:"info",className:"notyf__toast--info",background:"#5784E5",icon:{className:"notyf__icon--info",tagName:"i"}}),this.addTypeIfNotExists(e.types,{type:"warning",className:"notyf__toast--warning",background:"#E3A008",icon:{className:"notyf__icon--warning",tagName:"i"}}),this.notyf=this.notyf||new p(e)}initializeNotyf(){this.notyf||this.renderOptions({duration:1e4,position:{x:"right",y:"top"},dismissible:!0})}addTypeIfNotExists(t,e){t.some((t=>t.type===e.type))||t.push(e)}attachEventListeners(t,e){if(!this.notyf)return;const i=this.notyf;i.on("click",(({target:i,event:n})=>{i===t&&this.dispatchEvent("flasher:notyf:click",e,{event:n})})),i.on("dismiss",(({target:i,event:n})=>{i===t&&this.dispatchEvent("flasher:notyf:dismiss",e,{event:n})}))}dispatchEvent(t,e,i={}){window.dispatchEvent(new CustomEvent(t,{detail:Object.assign({envelope:e},i)}))}};return t.addPlugin("notyf",f),f})); diff --git a/src/Prime/Resources/assets/flasher-plugin.ts b/src/Prime/Resources/assets/flasher-plugin.ts index 52dce6a5..87879c62 100644 --- a/src/Prime/Resources/assets/flasher-plugin.ts +++ b/src/Prime/Resources/assets/flasher-plugin.ts @@ -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 + } } diff --git a/src/Prime/Resources/dist/flasher-plugin.d.ts b/src/Prime/Resources/dist/flasher-plugin.d.ts index 55895f22..b5867bd5 100644 --- a/src/Prime/Resources/dist/flasher-plugin.d.ts +++ b/src/Prime/Resources/dist/flasher-plugin.d.ts @@ -14,4 +14,6 @@ export default class FlasherPlugin extends AbstractPlugin { private removeNotification; private stringToHTML; private escapeHtml; + private dispatchClickEvents; + private getThemeName; } diff --git a/src/Prime/Resources/dist/flasher.esm.js b/src/Prime/Resources/dist/flasher.esm.js index 381813ec..8ccd2bb5 100644 --- a/src/Prime/Resources/dist/flasher.esm.js +++ b/src/Prime/Resources/dist/flasher.esm.js @@ -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 { diff --git a/src/Prime/Resources/dist/flasher.js b/src/Prime/Resources/dist/flasher.js index 7ade0d06..99796483 100644 --- a/src/Prime/Resources/dist/flasher.js +++ b/src/Prime/Resources/dist/flasher.js @@ -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 { diff --git a/src/Prime/Resources/dist/flasher.min.js b/src/Prime/Resources/dist/flasher.min.js index 52134cf8..c9c3adc6 100644 --- a/src/Prime/Resources/dist/flasher.min.js +++ b/src/Prime/Resources/dist/flasher.min.js @@ -1 +1 @@ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).flasher=t()}(this,(function(){"use strict";function e(e,t,s,n){return new(s||(s=Promise))((function(r,o){function i(e){try{a(n.next(e))}catch(e){o(e)}}function l(e){try{a(n.throw(e))}catch(e){o(e)}}function a(e){var t;e.done?r(e.value):(t=e.value,t instanceof s?t:new s((function(e){e(t)}))).then(i,l)}a((n=n.apply(e,t||[])).next())}))}"function"==typeof SuppressedError&&SuppressedError;class t{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,n){let r,o,i,l={};if("object"==typeof e?(l=Object.assign({},e),r=l.type,o=l.message,i=l.title,delete l.type,delete l.message,delete l.title):"object"==typeof t?(l=Object.assign({},t),r=e,o=l.message,i=l.title,delete l.message,delete l.title):(r=e,o=t,null==s?(i=void 0,l=n||{}):"string"==typeof s?(i=s,l=n||{}):"object"==typeof s&&(l=Object.assign({},s),"title"in l?(i=l.title,delete l.title):i=void 0,n&&"object"==typeof n&&(l=Object.assign(Object.assign({},l),n)))),!r)throw new Error("Type is required for notifications");if(null==o)throw new Error("Message is required for notifications");null==i&&(i=r.charAt(0).toUpperCase()+r.slice(1));const a={type:r,message:o,title:i,options:l,metadata:{plugin:""}};this.renderOptions({}),this.renderEnvelopes([a])}}class s extends t{constructor(e){if(super(),this.options={timeout:null,timeouts:{success:1e4,info:1e4,error:1e4,warning:1e4},fps:30,position:"top-right",direction:"top",rtl:!1,style:{},escapeHtml:!1},!e)throw new Error("Theme is required");if("function"!=typeof e.render)throw new TypeError("Theme must have a render function");this.theme=e}renderEnvelopes(e){if(!(null==e?void 0:e.length))return;const t=()=>{e.forEach((e=>{var t,s,n,r;try{const o=null!==(s=null!==(t=this.options.timeout)&&void 0!==t?t:this.options.timeouts[e.type])&&void 0!==s?s:1e4,i=Object.assign(Object.assign(Object.assign({},this.options),e.options),{timeout:this.normalizeTimeout(null!==(n=e.options.timeout)&&void 0!==n?n:o),escapeHtml:null!==(r=e.options.escapeHtml)&&void 0!==r?r:this.options.escapeHtml}),l=this.createContainer(i),a={direction:i.direction,timeout:Number(i.timeout||0),fps:i.fps,rtl:i.rtl,escapeHtml:i.escapeHtml};this.addToContainer(l,e,a)}catch(t){console.error("PHPFlasher: Error rendering envelope",t,e)}}))};if("loading"===document.readyState){const e=()=>{document.removeEventListener("DOMContentLoaded",e),t()};document.addEventListener("DOMContentLoaded",e)}else t()}renderOptions(e){e&&(this.options=Object.assign(Object.assign({},this.options),e))}createContainer(e){let t=document.querySelector(`.fl-wrapper[data-position="${e.position}"]`);return t||(t=document.createElement("div"),t.className="fl-wrapper",t.dataset.position=e.position,Object.entries(e.style).forEach((([e,s])=>{if(null!=s){const n=e.replace(/([A-Z])/g,"-$1").toLowerCase();t.style.setProperty(n,String(s))}})),document.body.appendChild(t)),t.dataset.turboTemporary="",t}addToContainer(e,t,s){s.escapeHtml&&(t.title=this.escapeHtml(t.title),t.message=this.escapeHtml(t.message));const n=this.stringToHTML(this.theme.render(t));n.classList.add("fl-container"),s.rtl&&n.classList.add("fl-rtl"),"bottom"===s.direction?e.append(n):e.prepend(n),requestAnimationFrame((()=>n.classList.add("fl-show")));const r=n.querySelector(".fl-close");if(r&&r.addEventListener("click",(e=>{e.stopPropagation(),this.removeNotification(n)})),s.timeout>0)this.addTimer(n,s);else{n.classList.add("fl-sticky");const e=n.querySelector(".fl-progress-bar");if(e){const t=document.createElement("span");t.classList.add("fl-progress","fl-sticky-progress"),t.style.width="100%",e.append(t)}}}normalizeTimeout(e){return!1===e||"number"==typeof e&&e<0||null==e?0:Number(e)||0}addTimer(e,{timeout:t,fps:s}){if(t<=0)return;const n=1e3/s;let r,o=0;const i=()=>{o+=n;const s=e.querySelector(".fl-progress-bar");if(s){let e=s.querySelector(".fl-progress");e||(e=document.createElement("span"),e.classList.add("fl-progress"),s.append(e));const n=100*(1-o/t);e.style.width=`${Math.max(0,n)}%`}o>=t&&(clearInterval(r),this.removeNotification(e))};r=window.setInterval(i,n);const l=()=>{clearInterval(r),r=window.setInterval(i,n)},a=()=>clearInterval(r);e.addEventListener("mouseout",l),e.addEventListener("mouseover",a),e._flasherCleanup=()=>{clearInterval(r),e.removeEventListener("mouseout",l),e.removeEventListener("mouseover",a)}}removeNotification(e){e&&(e._flasherCleanup&&(e._flasherCleanup(),delete e._flasherCleanup),e.classList.remove("fl-show"),e.ontransitionend=()=>{const t=e.parentElement;e.remove(),t&&!t.hasChildNodes()&&t.remove()})}stringToHTML(e){const t=document.createElement("template");t.innerHTML=e.trim();const s=t.content.firstElementChild;if(!s)throw new Error("PHPFlasher: Invalid HTML template - no element found");return s}escapeHtml(e){if(null==e)return"";const t={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`","=":"=","/":"/"};return e.replace(/[&<>"'`=/]/g,(e=>t[e]||e))}}const n=new class extends t{constructor(){super(...arguments),this.defaultPlugin="flasher",this.plugins=new Map,this.themes=new Map,this.loadedAssets=new Set}render(t){return e(this,void 0,void 0,(function*(){const e=this.resolveResponse(t);try{yield this.addAssets([{urls:e.styles,nonce:e.context.csp_style_nonce,type:"style"},{urls:e.scripts,nonce:e.context.csp_script_nonce,type:"script"}]),this.renderOptions(e.options),this.renderEnvelopes(e.envelopes)}catch(e){console.error("PHPFlasher: Error rendering notifications",e)}}))}renderEnvelopes(e){if(!(null==e?void 0:e.length))return;const t={};e.forEach((e=>{const s=this.resolvePluginAlias(e.metadata.plugin);t[s]=t[s]||[],t[s].push(e)})),Object.entries(t).forEach((([e,t])=>{try{this.use(e).renderEnvelopes(t)}catch(t){console.error(`PHPFlasher: Error rendering envelopes for plugin "${e}"`,t)}}))}renderOptions(e){e&&Object.entries(e).forEach((([e,t])=>{try{this.use(e).renderOptions(t)}catch(t){console.error(`PHPFlasher: Error applying options for plugin "${e}"`,t)}}))}addPlugin(e,t){if(!e||!t)throw new Error("Both plugin name and instance are required");this.plugins.set(e,t)}addTheme(e,t){if(!e||!t)throw new Error("Both theme name and definition are required");this.themes.set(e,t)}use(e){const t=this.resolvePluginAlias(e);this.resolvePlugin(t);const s=this.plugins.get(t);if(!s)throw new Error(`Unable to resolve "${t}" plugin, did you forget to register it?`);return s}create(e){return this.use(e)}resolveResponse(e){const t=Object.assign({envelopes:[],options:{},scripts:[],styles:[],context:{}},e);return Object.entries(t.options).forEach((([e,s])=>{t.options[e]=this.resolveOptions(s)})),t.context.csp_style_nonce=t.context.csp_style_nonce||"",t.context.csp_script_nonce=t.context.csp_script_nonce||"",t.envelopes.forEach((e=>{e.metadata=e.metadata||{},e.metadata.plugin=this.resolvePluginAlias(e.metadata.plugin),this.addThemeStyles(t,e.metadata.plugin),e.options=this.resolveOptions(e.options),e.context=t.context})),t}resolveOptions(e){if(!e)return{};const t=Object.assign({},e);return Object.entries(t).forEach((([e,s])=>{t[e]=this.resolveFunction(s)})),t}resolveFunction(e){var t,s,n,r;if("string"!=typeof e)return e;const o=e.match(/^function\s*(\w*)\s*\(([^)]*)\)\s*\{([\s\S]*)\}$/),i=e.match(/^\s*(\(([^)]*)\)|[^=]+)\s*=>\s*([\s\S]+)$/);if(!o&&!i)return e;let l,a;o?(l=null!==(s=null===(t=o[2])||void 0===t?void 0:t.split(",").map((e=>e.trim())).filter(Boolean))&&void 0!==s?s:[],a=o[3].trim()):(l=null!==(r=null===(n=i[2])||void 0===n?void 0:n.split(",").map((e=>e.trim())).filter(Boolean))&&void 0!==r?r:[],a=i[3].trim(),a=a.startsWith("{")?a.slice(1,-1).trim():`return ${a};`);try{return new Function(...l,a)}catch(t){return console.error("PHPFlasher: Error converting string to function:",t),e}}resolvePlugin(e){if(this.plugins.get(e)||!e.includes("theme."))return;const t=e.replace("theme.",""),n=this.themes.get(t);n&&this.addPlugin(e,new s(n))}resolvePluginAlias(e){return"flasher"===(e=e||this.defaultPlugin)?"theme.flasher":e}addAssets(t){return e(this,void 0,void 0,(function*(){try{const e=t.filter((e=>"style"===e.type)),s=[];for(const{urls:t,nonce:n,type:r}of e)if(null==t?void 0:t.length)for(const e of t)e&&!this.loadedAssets.has(e)&&(s.push(this.loadAsset(e,n,r)),this.loadedAssets.add(e));yield Promise.all(s);const n=t.filter((e=>"script"===e.type));for(const{urls:e,nonce:t,type:s}of n)if(null==e?void 0:e.length)for(const n of e)n&&!this.loadedAssets.has(n)&&(yield this.loadAsset(n,t,s),this.loadedAssets.add(n))}catch(e){console.error("PHPFlasher: Error loading assets",e)}}))}loadAsset(e,t,s){const n="style"===s?`link[href="${e}"]`:`script[src="${e}"]`;return document.querySelector(n)?Promise.resolve():new Promise(((n,r)=>{const o=document.createElement("style"===s?"link":"script");"style"===s?(o.rel="stylesheet",o.href=e):(o.type="text/javascript",o.src=e),t&&o.setAttribute("nonce",t),o.onload=()=>n(),o.onerror=()=>r(new Error(`Failed to load ${e}`)),document.head.appendChild(o)}))}addThemeStyles(e,t){if("flasher"!==t&&!t.includes("theme."))return;const s=t.replace("theme.",""),n=this.themes.get(s);if(!(null==n?void 0:n.styles))return;const r=Array.isArray(n.styles)?n.styles:[n.styles];e.styles=Array.from(new Set([...e.styles,...r]))}};return n.addTheme("flasher",{render:e=>{const{type:t,title:s,message:n}=e,r="error"===t||"warning"===t,o=r?"alert":"status",i=r?"assertive":"polite",l=s||t.charAt(0).toUpperCase()+t.slice(1);return`\n
\n
\n
\n
\n ${l}\n ${n}\n
\n \n
\n \n \n \n
`}}),"undefined"!=typeof window&&(window.flasher=n),n})); +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).flasher=t()}(this,(function(){"use strict";function e(e,t,s,n){return new(s||(s=Promise))((function(r,o){function i(e){try{a(n.next(e))}catch(e){o(e)}}function l(e){try{a(n.throw(e))}catch(e){o(e)}}function a(e){var t;e.done?r(e.value):(t=e.value,t instanceof s?t:new s((function(e){e(t)}))).then(i,l)}a((n=n.apply(e,t||[])).next())}))}"function"==typeof SuppressedError&&SuppressedError;class t{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,n){let r,o,i,l={};if("object"==typeof e?(l=Object.assign({},e),r=l.type,o=l.message,i=l.title,delete l.type,delete l.message,delete l.title):"object"==typeof t?(l=Object.assign({},t),r=e,o=l.message,i=l.title,delete l.message,delete l.title):(r=e,o=t,null==s?(i=void 0,l=n||{}):"string"==typeof s?(i=s,l=n||{}):"object"==typeof s&&(l=Object.assign({},s),"title"in l?(i=l.title,delete l.title):i=void 0,n&&"object"==typeof n&&(l=Object.assign(Object.assign({},l),n)))),!r)throw new Error("Type is required for notifications");if(null==o)throw new Error("Message is required for notifications");null==i&&(i=r.charAt(0).toUpperCase()+r.slice(1));const a={type:r,message:o,title:i,options:l,metadata:{plugin:""}};this.renderOptions({}),this.renderEnvelopes([a])}}class s extends t{constructor(e){if(super(),this.options={timeout:null,timeouts:{success:1e4,info:1e4,error:1e4,warning:1e4},fps:30,position:"top-right",direction:"top",rtl:!1,style:{},escapeHtml:!1},!e)throw new Error("Theme is required");if("function"!=typeof e.render)throw new TypeError("Theme must have a render function");this.theme=e}renderEnvelopes(e){if(!(null==e?void 0:e.length))return;const t=()=>{e.forEach((e=>{var t,s,n,r;try{const o=null!==(s=null!==(t=this.options.timeout)&&void 0!==t?t:this.options.timeouts[e.type])&&void 0!==s?s:1e4,i=Object.assign(Object.assign(Object.assign({},this.options),e.options),{timeout:this.normalizeTimeout(null!==(n=e.options.timeout)&&void 0!==n?n:o),escapeHtml:null!==(r=e.options.escapeHtml)&&void 0!==r?r:this.options.escapeHtml}),l=this.createContainer(i),a={direction:i.direction,timeout:Number(i.timeout||0),fps:i.fps,rtl:i.rtl,escapeHtml:i.escapeHtml};this.addToContainer(l,e,a)}catch(t){console.error("PHPFlasher: Error rendering envelope",t,e)}}))};if("loading"===document.readyState){const e=()=>{document.removeEventListener("DOMContentLoaded",e),t()};document.addEventListener("DOMContentLoaded",e)}else t()}renderOptions(e){e&&(this.options=Object.assign(Object.assign({},this.options),e))}createContainer(e){let t=document.querySelector(`.fl-wrapper[data-position="${e.position}"]`);return t||(t=document.createElement("div"),t.className="fl-wrapper",t.dataset.position=e.position,Object.entries(e.style).forEach((([e,s])=>{if(null!=s){const n=e.replace(/([A-Z])/g,"-$1").toLowerCase();t.style.setProperty(n,String(s))}})),document.body.appendChild(t)),t.dataset.turboTemporary="",t}addToContainer(e,t,s){s.escapeHtml&&(t.title=this.escapeHtml(t.title),t.message=this.escapeHtml(t.message));const n=this.stringToHTML(this.theme.render(t));n.classList.add("fl-container"),s.rtl&&n.classList.add("fl-rtl"),"bottom"===s.direction?e.append(n):e.prepend(n),requestAnimationFrame((()=>n.classList.add("fl-show")));const r=n.querySelector(".fl-close");if(r&&r.addEventListener("click",(e=>{e.stopPropagation(),this.removeNotification(n)})),n.addEventListener("click",(e=>{e.target.closest(".fl-close")||this.dispatchClickEvents(t)})),s.timeout>0)this.addTimer(n,s);else{n.classList.add("fl-sticky");const e=n.querySelector(".fl-progress-bar");if(e){const t=document.createElement("span");t.classList.add("fl-progress","fl-sticky-progress"),t.style.width="100%",e.append(t)}}}normalizeTimeout(e){return!1===e||"number"==typeof e&&e<0||null==e?0:Number(e)||0}addTimer(e,{timeout:t,fps:s}){if(t<=0)return;const n=1e3/s;let r,o=0;const i=()=>{o+=n;const s=e.querySelector(".fl-progress-bar");if(s){let e=s.querySelector(".fl-progress");e||(e=document.createElement("span"),e.classList.add("fl-progress"),s.append(e));const n=100*(1-o/t);e.style.width=`${Math.max(0,n)}%`}o>=t&&(clearInterval(r),this.removeNotification(e))};r=window.setInterval(i,n);const l=()=>{clearInterval(r),r=window.setInterval(i,n)},a=()=>clearInterval(r);e.addEventListener("mouseout",l),e.addEventListener("mouseover",a),e._flasherCleanup=()=>{clearInterval(r),e.removeEventListener("mouseout",l),e.removeEventListener("mouseover",a)}}removeNotification(e){e&&(e._flasherCleanup&&(e._flasherCleanup(),delete e._flasherCleanup),e.classList.remove("fl-show"),e.ontransitionend=()=>{const t=e.parentElement;e.remove(),t&&!t.hasChildNodes()&&t.remove()})}stringToHTML(e){const t=document.createElement("template");t.innerHTML=e.trim();const s=t.content.firstElementChild;if(!s)throw new Error("PHPFlasher: Invalid HTML template - no element found");return s}escapeHtml(e){if(null==e)return"";const t={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`","=":"=","/":"/"};return e.replace(/[&<>"'`=/]/g,(e=>t[e]||e))}dispatchClickEvents(e){const t={envelope:e};window.dispatchEvent(new CustomEvent("flasher:theme:click",{detail:t}));const s=this.getThemeName(e);s&&window.dispatchEvent(new CustomEvent(`flasher:theme:${s}:click`,{detail:t}))}getThemeName(e){var t;const s=(null===(t=e.metadata)||void 0===t?void 0:t.plugin)||"";return s.startsWith("theme.")?s.replace("theme.",""):"flasher"===s?"flasher":s}}const n=new class extends t{constructor(){super(...arguments),this.defaultPlugin="flasher",this.plugins=new Map,this.themes=new Map,this.loadedAssets=new Set}render(t){return e(this,void 0,void 0,(function*(){const e=this.resolveResponse(t);try{yield this.addAssets([{urls:e.styles,nonce:e.context.csp_style_nonce,type:"style"},{urls:e.scripts,nonce:e.context.csp_script_nonce,type:"script"}]),this.renderOptions(e.options),this.renderEnvelopes(e.envelopes)}catch(e){console.error("PHPFlasher: Error rendering notifications",e)}}))}renderEnvelopes(e){if(!(null==e?void 0:e.length))return;const t={};e.forEach((e=>{const s=this.resolvePluginAlias(e.metadata.plugin);t[s]=t[s]||[],t[s].push(e)})),Object.entries(t).forEach((([e,t])=>{try{this.use(e).renderEnvelopes(t)}catch(t){console.error(`PHPFlasher: Error rendering envelopes for plugin "${e}"`,t)}}))}renderOptions(e){e&&Object.entries(e).forEach((([e,t])=>{try{this.use(e).renderOptions(t)}catch(t){console.error(`PHPFlasher: Error applying options for plugin "${e}"`,t)}}))}addPlugin(e,t){if(!e||!t)throw new Error("Both plugin name and instance are required");this.plugins.set(e,t)}addTheme(e,t){if(!e||!t)throw new Error("Both theme name and definition are required");this.themes.set(e,t)}use(e){const t=this.resolvePluginAlias(e);this.resolvePlugin(t);const s=this.plugins.get(t);if(!s)throw new Error(`Unable to resolve "${t}" plugin, did you forget to register it?`);return s}create(e){return this.use(e)}resolveResponse(e){const t=Object.assign({envelopes:[],options:{},scripts:[],styles:[],context:{}},e);return Object.entries(t.options).forEach((([e,s])=>{t.options[e]=this.resolveOptions(s)})),t.context.csp_style_nonce=t.context.csp_style_nonce||"",t.context.csp_script_nonce=t.context.csp_script_nonce||"",t.envelopes.forEach((e=>{e.metadata=e.metadata||{},e.metadata.plugin=this.resolvePluginAlias(e.metadata.plugin),this.addThemeStyles(t,e.metadata.plugin),e.options=this.resolveOptions(e.options),e.context=t.context})),t}resolveOptions(e){if(!e)return{};const t=Object.assign({},e);return Object.entries(t).forEach((([e,s])=>{t[e]=this.resolveFunction(s)})),t}resolveFunction(e){var t,s,n,r;if("string"!=typeof e)return e;const o=e.match(/^function\s*(\w*)\s*\(([^)]*)\)\s*\{([\s\S]*)\}$/),i=e.match(/^\s*(\(([^)]*)\)|[^=]+)\s*=>\s*([\s\S]+)$/);if(!o&&!i)return e;let l,a;o?(l=null!==(s=null===(t=o[2])||void 0===t?void 0:t.split(",").map((e=>e.trim())).filter(Boolean))&&void 0!==s?s:[],a=o[3].trim()):(l=null!==(r=null===(n=i[2])||void 0===n?void 0:n.split(",").map((e=>e.trim())).filter(Boolean))&&void 0!==r?r:[],a=i[3].trim(),a=a.startsWith("{")?a.slice(1,-1).trim():`return ${a};`);try{return new Function(...l,a)}catch(t){return console.error("PHPFlasher: Error converting string to function:",t),e}}resolvePlugin(e){if(this.plugins.get(e)||!e.includes("theme."))return;const t=e.replace("theme.",""),n=this.themes.get(t);n&&this.addPlugin(e,new s(n))}resolvePluginAlias(e){return"flasher"===(e=e||this.defaultPlugin)?"theme.flasher":e}addAssets(t){return e(this,void 0,void 0,(function*(){try{const e=t.filter((e=>"style"===e.type)),s=[];for(const{urls:t,nonce:n,type:r}of e)if(null==t?void 0:t.length)for(const e of t)e&&!this.loadedAssets.has(e)&&(s.push(this.loadAsset(e,n,r)),this.loadedAssets.add(e));yield Promise.all(s);const n=t.filter((e=>"script"===e.type));for(const{urls:e,nonce:t,type:s}of n)if(null==e?void 0:e.length)for(const n of e)n&&!this.loadedAssets.has(n)&&(yield this.loadAsset(n,t,s),this.loadedAssets.add(n))}catch(e){console.error("PHPFlasher: Error loading assets",e)}}))}loadAsset(e,t,s){const n="style"===s?`link[href="${e}"]`:`script[src="${e}"]`;return document.querySelector(n)?Promise.resolve():new Promise(((n,r)=>{const o=document.createElement("style"===s?"link":"script");"style"===s?(o.rel="stylesheet",o.href=e):(o.type="text/javascript",o.src=e),t&&o.setAttribute("nonce",t),o.onload=()=>n(),o.onerror=()=>r(new Error(`Failed to load ${e}`)),document.head.appendChild(o)}))}addThemeStyles(e,t){if("flasher"!==t&&!t.includes("theme."))return;const s=t.replace("theme.",""),n=this.themes.get(s);if(!(null==n?void 0:n.styles))return;const r=Array.isArray(n.styles)?n.styles:[n.styles];e.styles=Array.from(new Set([...e.styles,...r]))}};return n.addTheme("flasher",{render:e=>{const{type:t,title:s,message:n}=e,r="error"===t||"warning"===t,o=r?"alert":"status",i=r?"assertive":"polite",l=s||t.charAt(0).toUpperCase()+t.slice(1);return`\n
\n
\n
\n
\n ${l}\n ${n}\n
\n \n
\n \n \n \n
`}}),"undefined"!=typeof window&&(window.flasher=n),n})); diff --git a/src/Prime/Resources/public/flasher.min.js b/src/Prime/Resources/public/flasher.min.js index 52134cf8..c9c3adc6 100644 --- a/src/Prime/Resources/public/flasher.min.js +++ b/src/Prime/Resources/public/flasher.min.js @@ -1 +1 @@ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).flasher=t()}(this,(function(){"use strict";function e(e,t,s,n){return new(s||(s=Promise))((function(r,o){function i(e){try{a(n.next(e))}catch(e){o(e)}}function l(e){try{a(n.throw(e))}catch(e){o(e)}}function a(e){var t;e.done?r(e.value):(t=e.value,t instanceof s?t:new s((function(e){e(t)}))).then(i,l)}a((n=n.apply(e,t||[])).next())}))}"function"==typeof SuppressedError&&SuppressedError;class t{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,n){let r,o,i,l={};if("object"==typeof e?(l=Object.assign({},e),r=l.type,o=l.message,i=l.title,delete l.type,delete l.message,delete l.title):"object"==typeof t?(l=Object.assign({},t),r=e,o=l.message,i=l.title,delete l.message,delete l.title):(r=e,o=t,null==s?(i=void 0,l=n||{}):"string"==typeof s?(i=s,l=n||{}):"object"==typeof s&&(l=Object.assign({},s),"title"in l?(i=l.title,delete l.title):i=void 0,n&&"object"==typeof n&&(l=Object.assign(Object.assign({},l),n)))),!r)throw new Error("Type is required for notifications");if(null==o)throw new Error("Message is required for notifications");null==i&&(i=r.charAt(0).toUpperCase()+r.slice(1));const a={type:r,message:o,title:i,options:l,metadata:{plugin:""}};this.renderOptions({}),this.renderEnvelopes([a])}}class s extends t{constructor(e){if(super(),this.options={timeout:null,timeouts:{success:1e4,info:1e4,error:1e4,warning:1e4},fps:30,position:"top-right",direction:"top",rtl:!1,style:{},escapeHtml:!1},!e)throw new Error("Theme is required");if("function"!=typeof e.render)throw new TypeError("Theme must have a render function");this.theme=e}renderEnvelopes(e){if(!(null==e?void 0:e.length))return;const t=()=>{e.forEach((e=>{var t,s,n,r;try{const o=null!==(s=null!==(t=this.options.timeout)&&void 0!==t?t:this.options.timeouts[e.type])&&void 0!==s?s:1e4,i=Object.assign(Object.assign(Object.assign({},this.options),e.options),{timeout:this.normalizeTimeout(null!==(n=e.options.timeout)&&void 0!==n?n:o),escapeHtml:null!==(r=e.options.escapeHtml)&&void 0!==r?r:this.options.escapeHtml}),l=this.createContainer(i),a={direction:i.direction,timeout:Number(i.timeout||0),fps:i.fps,rtl:i.rtl,escapeHtml:i.escapeHtml};this.addToContainer(l,e,a)}catch(t){console.error("PHPFlasher: Error rendering envelope",t,e)}}))};if("loading"===document.readyState){const e=()=>{document.removeEventListener("DOMContentLoaded",e),t()};document.addEventListener("DOMContentLoaded",e)}else t()}renderOptions(e){e&&(this.options=Object.assign(Object.assign({},this.options),e))}createContainer(e){let t=document.querySelector(`.fl-wrapper[data-position="${e.position}"]`);return t||(t=document.createElement("div"),t.className="fl-wrapper",t.dataset.position=e.position,Object.entries(e.style).forEach((([e,s])=>{if(null!=s){const n=e.replace(/([A-Z])/g,"-$1").toLowerCase();t.style.setProperty(n,String(s))}})),document.body.appendChild(t)),t.dataset.turboTemporary="",t}addToContainer(e,t,s){s.escapeHtml&&(t.title=this.escapeHtml(t.title),t.message=this.escapeHtml(t.message));const n=this.stringToHTML(this.theme.render(t));n.classList.add("fl-container"),s.rtl&&n.classList.add("fl-rtl"),"bottom"===s.direction?e.append(n):e.prepend(n),requestAnimationFrame((()=>n.classList.add("fl-show")));const r=n.querySelector(".fl-close");if(r&&r.addEventListener("click",(e=>{e.stopPropagation(),this.removeNotification(n)})),s.timeout>0)this.addTimer(n,s);else{n.classList.add("fl-sticky");const e=n.querySelector(".fl-progress-bar");if(e){const t=document.createElement("span");t.classList.add("fl-progress","fl-sticky-progress"),t.style.width="100%",e.append(t)}}}normalizeTimeout(e){return!1===e||"number"==typeof e&&e<0||null==e?0:Number(e)||0}addTimer(e,{timeout:t,fps:s}){if(t<=0)return;const n=1e3/s;let r,o=0;const i=()=>{o+=n;const s=e.querySelector(".fl-progress-bar");if(s){let e=s.querySelector(".fl-progress");e||(e=document.createElement("span"),e.classList.add("fl-progress"),s.append(e));const n=100*(1-o/t);e.style.width=`${Math.max(0,n)}%`}o>=t&&(clearInterval(r),this.removeNotification(e))};r=window.setInterval(i,n);const l=()=>{clearInterval(r),r=window.setInterval(i,n)},a=()=>clearInterval(r);e.addEventListener("mouseout",l),e.addEventListener("mouseover",a),e._flasherCleanup=()=>{clearInterval(r),e.removeEventListener("mouseout",l),e.removeEventListener("mouseover",a)}}removeNotification(e){e&&(e._flasherCleanup&&(e._flasherCleanup(),delete e._flasherCleanup),e.classList.remove("fl-show"),e.ontransitionend=()=>{const t=e.parentElement;e.remove(),t&&!t.hasChildNodes()&&t.remove()})}stringToHTML(e){const t=document.createElement("template");t.innerHTML=e.trim();const s=t.content.firstElementChild;if(!s)throw new Error("PHPFlasher: Invalid HTML template - no element found");return s}escapeHtml(e){if(null==e)return"";const t={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`","=":"=","/":"/"};return e.replace(/[&<>"'`=/]/g,(e=>t[e]||e))}}const n=new class extends t{constructor(){super(...arguments),this.defaultPlugin="flasher",this.plugins=new Map,this.themes=new Map,this.loadedAssets=new Set}render(t){return e(this,void 0,void 0,(function*(){const e=this.resolveResponse(t);try{yield this.addAssets([{urls:e.styles,nonce:e.context.csp_style_nonce,type:"style"},{urls:e.scripts,nonce:e.context.csp_script_nonce,type:"script"}]),this.renderOptions(e.options),this.renderEnvelopes(e.envelopes)}catch(e){console.error("PHPFlasher: Error rendering notifications",e)}}))}renderEnvelopes(e){if(!(null==e?void 0:e.length))return;const t={};e.forEach((e=>{const s=this.resolvePluginAlias(e.metadata.plugin);t[s]=t[s]||[],t[s].push(e)})),Object.entries(t).forEach((([e,t])=>{try{this.use(e).renderEnvelopes(t)}catch(t){console.error(`PHPFlasher: Error rendering envelopes for plugin "${e}"`,t)}}))}renderOptions(e){e&&Object.entries(e).forEach((([e,t])=>{try{this.use(e).renderOptions(t)}catch(t){console.error(`PHPFlasher: Error applying options for plugin "${e}"`,t)}}))}addPlugin(e,t){if(!e||!t)throw new Error("Both plugin name and instance are required");this.plugins.set(e,t)}addTheme(e,t){if(!e||!t)throw new Error("Both theme name and definition are required");this.themes.set(e,t)}use(e){const t=this.resolvePluginAlias(e);this.resolvePlugin(t);const s=this.plugins.get(t);if(!s)throw new Error(`Unable to resolve "${t}" plugin, did you forget to register it?`);return s}create(e){return this.use(e)}resolveResponse(e){const t=Object.assign({envelopes:[],options:{},scripts:[],styles:[],context:{}},e);return Object.entries(t.options).forEach((([e,s])=>{t.options[e]=this.resolveOptions(s)})),t.context.csp_style_nonce=t.context.csp_style_nonce||"",t.context.csp_script_nonce=t.context.csp_script_nonce||"",t.envelopes.forEach((e=>{e.metadata=e.metadata||{},e.metadata.plugin=this.resolvePluginAlias(e.metadata.plugin),this.addThemeStyles(t,e.metadata.plugin),e.options=this.resolveOptions(e.options),e.context=t.context})),t}resolveOptions(e){if(!e)return{};const t=Object.assign({},e);return Object.entries(t).forEach((([e,s])=>{t[e]=this.resolveFunction(s)})),t}resolveFunction(e){var t,s,n,r;if("string"!=typeof e)return e;const o=e.match(/^function\s*(\w*)\s*\(([^)]*)\)\s*\{([\s\S]*)\}$/),i=e.match(/^\s*(\(([^)]*)\)|[^=]+)\s*=>\s*([\s\S]+)$/);if(!o&&!i)return e;let l,a;o?(l=null!==(s=null===(t=o[2])||void 0===t?void 0:t.split(",").map((e=>e.trim())).filter(Boolean))&&void 0!==s?s:[],a=o[3].trim()):(l=null!==(r=null===(n=i[2])||void 0===n?void 0:n.split(",").map((e=>e.trim())).filter(Boolean))&&void 0!==r?r:[],a=i[3].trim(),a=a.startsWith("{")?a.slice(1,-1).trim():`return ${a};`);try{return new Function(...l,a)}catch(t){return console.error("PHPFlasher: Error converting string to function:",t),e}}resolvePlugin(e){if(this.plugins.get(e)||!e.includes("theme."))return;const t=e.replace("theme.",""),n=this.themes.get(t);n&&this.addPlugin(e,new s(n))}resolvePluginAlias(e){return"flasher"===(e=e||this.defaultPlugin)?"theme.flasher":e}addAssets(t){return e(this,void 0,void 0,(function*(){try{const e=t.filter((e=>"style"===e.type)),s=[];for(const{urls:t,nonce:n,type:r}of e)if(null==t?void 0:t.length)for(const e of t)e&&!this.loadedAssets.has(e)&&(s.push(this.loadAsset(e,n,r)),this.loadedAssets.add(e));yield Promise.all(s);const n=t.filter((e=>"script"===e.type));for(const{urls:e,nonce:t,type:s}of n)if(null==e?void 0:e.length)for(const n of e)n&&!this.loadedAssets.has(n)&&(yield this.loadAsset(n,t,s),this.loadedAssets.add(n))}catch(e){console.error("PHPFlasher: Error loading assets",e)}}))}loadAsset(e,t,s){const n="style"===s?`link[href="${e}"]`:`script[src="${e}"]`;return document.querySelector(n)?Promise.resolve():new Promise(((n,r)=>{const o=document.createElement("style"===s?"link":"script");"style"===s?(o.rel="stylesheet",o.href=e):(o.type="text/javascript",o.src=e),t&&o.setAttribute("nonce",t),o.onload=()=>n(),o.onerror=()=>r(new Error(`Failed to load ${e}`)),document.head.appendChild(o)}))}addThemeStyles(e,t){if("flasher"!==t&&!t.includes("theme."))return;const s=t.replace("theme.",""),n=this.themes.get(s);if(!(null==n?void 0:n.styles))return;const r=Array.isArray(n.styles)?n.styles:[n.styles];e.styles=Array.from(new Set([...e.styles,...r]))}};return n.addTheme("flasher",{render:e=>{const{type:t,title:s,message:n}=e,r="error"===t||"warning"===t,o=r?"alert":"status",i=r?"assertive":"polite",l=s||t.charAt(0).toUpperCase()+t.slice(1);return`\n
\n
\n
\n
\n ${l}\n ${n}\n
\n \n
\n \n \n \n
`}}),"undefined"!=typeof window&&(window.flasher=n),n})); +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).flasher=t()}(this,(function(){"use strict";function e(e,t,s,n){return new(s||(s=Promise))((function(r,o){function i(e){try{a(n.next(e))}catch(e){o(e)}}function l(e){try{a(n.throw(e))}catch(e){o(e)}}function a(e){var t;e.done?r(e.value):(t=e.value,t instanceof s?t:new s((function(e){e(t)}))).then(i,l)}a((n=n.apply(e,t||[])).next())}))}"function"==typeof SuppressedError&&SuppressedError;class t{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,n){let r,o,i,l={};if("object"==typeof e?(l=Object.assign({},e),r=l.type,o=l.message,i=l.title,delete l.type,delete l.message,delete l.title):"object"==typeof t?(l=Object.assign({},t),r=e,o=l.message,i=l.title,delete l.message,delete l.title):(r=e,o=t,null==s?(i=void 0,l=n||{}):"string"==typeof s?(i=s,l=n||{}):"object"==typeof s&&(l=Object.assign({},s),"title"in l?(i=l.title,delete l.title):i=void 0,n&&"object"==typeof n&&(l=Object.assign(Object.assign({},l),n)))),!r)throw new Error("Type is required for notifications");if(null==o)throw new Error("Message is required for notifications");null==i&&(i=r.charAt(0).toUpperCase()+r.slice(1));const a={type:r,message:o,title:i,options:l,metadata:{plugin:""}};this.renderOptions({}),this.renderEnvelopes([a])}}class s extends t{constructor(e){if(super(),this.options={timeout:null,timeouts:{success:1e4,info:1e4,error:1e4,warning:1e4},fps:30,position:"top-right",direction:"top",rtl:!1,style:{},escapeHtml:!1},!e)throw new Error("Theme is required");if("function"!=typeof e.render)throw new TypeError("Theme must have a render function");this.theme=e}renderEnvelopes(e){if(!(null==e?void 0:e.length))return;const t=()=>{e.forEach((e=>{var t,s,n,r;try{const o=null!==(s=null!==(t=this.options.timeout)&&void 0!==t?t:this.options.timeouts[e.type])&&void 0!==s?s:1e4,i=Object.assign(Object.assign(Object.assign({},this.options),e.options),{timeout:this.normalizeTimeout(null!==(n=e.options.timeout)&&void 0!==n?n:o),escapeHtml:null!==(r=e.options.escapeHtml)&&void 0!==r?r:this.options.escapeHtml}),l=this.createContainer(i),a={direction:i.direction,timeout:Number(i.timeout||0),fps:i.fps,rtl:i.rtl,escapeHtml:i.escapeHtml};this.addToContainer(l,e,a)}catch(t){console.error("PHPFlasher: Error rendering envelope",t,e)}}))};if("loading"===document.readyState){const e=()=>{document.removeEventListener("DOMContentLoaded",e),t()};document.addEventListener("DOMContentLoaded",e)}else t()}renderOptions(e){e&&(this.options=Object.assign(Object.assign({},this.options),e))}createContainer(e){let t=document.querySelector(`.fl-wrapper[data-position="${e.position}"]`);return t||(t=document.createElement("div"),t.className="fl-wrapper",t.dataset.position=e.position,Object.entries(e.style).forEach((([e,s])=>{if(null!=s){const n=e.replace(/([A-Z])/g,"-$1").toLowerCase();t.style.setProperty(n,String(s))}})),document.body.appendChild(t)),t.dataset.turboTemporary="",t}addToContainer(e,t,s){s.escapeHtml&&(t.title=this.escapeHtml(t.title),t.message=this.escapeHtml(t.message));const n=this.stringToHTML(this.theme.render(t));n.classList.add("fl-container"),s.rtl&&n.classList.add("fl-rtl"),"bottom"===s.direction?e.append(n):e.prepend(n),requestAnimationFrame((()=>n.classList.add("fl-show")));const r=n.querySelector(".fl-close");if(r&&r.addEventListener("click",(e=>{e.stopPropagation(),this.removeNotification(n)})),n.addEventListener("click",(e=>{e.target.closest(".fl-close")||this.dispatchClickEvents(t)})),s.timeout>0)this.addTimer(n,s);else{n.classList.add("fl-sticky");const e=n.querySelector(".fl-progress-bar");if(e){const t=document.createElement("span");t.classList.add("fl-progress","fl-sticky-progress"),t.style.width="100%",e.append(t)}}}normalizeTimeout(e){return!1===e||"number"==typeof e&&e<0||null==e?0:Number(e)||0}addTimer(e,{timeout:t,fps:s}){if(t<=0)return;const n=1e3/s;let r,o=0;const i=()=>{o+=n;const s=e.querySelector(".fl-progress-bar");if(s){let e=s.querySelector(".fl-progress");e||(e=document.createElement("span"),e.classList.add("fl-progress"),s.append(e));const n=100*(1-o/t);e.style.width=`${Math.max(0,n)}%`}o>=t&&(clearInterval(r),this.removeNotification(e))};r=window.setInterval(i,n);const l=()=>{clearInterval(r),r=window.setInterval(i,n)},a=()=>clearInterval(r);e.addEventListener("mouseout",l),e.addEventListener("mouseover",a),e._flasherCleanup=()=>{clearInterval(r),e.removeEventListener("mouseout",l),e.removeEventListener("mouseover",a)}}removeNotification(e){e&&(e._flasherCleanup&&(e._flasherCleanup(),delete e._flasherCleanup),e.classList.remove("fl-show"),e.ontransitionend=()=>{const t=e.parentElement;e.remove(),t&&!t.hasChildNodes()&&t.remove()})}stringToHTML(e){const t=document.createElement("template");t.innerHTML=e.trim();const s=t.content.firstElementChild;if(!s)throw new Error("PHPFlasher: Invalid HTML template - no element found");return s}escapeHtml(e){if(null==e)return"";const t={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`","=":"=","/":"/"};return e.replace(/[&<>"'`=/]/g,(e=>t[e]||e))}dispatchClickEvents(e){const t={envelope:e};window.dispatchEvent(new CustomEvent("flasher:theme:click",{detail:t}));const s=this.getThemeName(e);s&&window.dispatchEvent(new CustomEvent(`flasher:theme:${s}:click`,{detail:t}))}getThemeName(e){var t;const s=(null===(t=e.metadata)||void 0===t?void 0:t.plugin)||"";return s.startsWith("theme.")?s.replace("theme.",""):"flasher"===s?"flasher":s}}const n=new class extends t{constructor(){super(...arguments),this.defaultPlugin="flasher",this.plugins=new Map,this.themes=new Map,this.loadedAssets=new Set}render(t){return e(this,void 0,void 0,(function*(){const e=this.resolveResponse(t);try{yield this.addAssets([{urls:e.styles,nonce:e.context.csp_style_nonce,type:"style"},{urls:e.scripts,nonce:e.context.csp_script_nonce,type:"script"}]),this.renderOptions(e.options),this.renderEnvelopes(e.envelopes)}catch(e){console.error("PHPFlasher: Error rendering notifications",e)}}))}renderEnvelopes(e){if(!(null==e?void 0:e.length))return;const t={};e.forEach((e=>{const s=this.resolvePluginAlias(e.metadata.plugin);t[s]=t[s]||[],t[s].push(e)})),Object.entries(t).forEach((([e,t])=>{try{this.use(e).renderEnvelopes(t)}catch(t){console.error(`PHPFlasher: Error rendering envelopes for plugin "${e}"`,t)}}))}renderOptions(e){e&&Object.entries(e).forEach((([e,t])=>{try{this.use(e).renderOptions(t)}catch(t){console.error(`PHPFlasher: Error applying options for plugin "${e}"`,t)}}))}addPlugin(e,t){if(!e||!t)throw new Error("Both plugin name and instance are required");this.plugins.set(e,t)}addTheme(e,t){if(!e||!t)throw new Error("Both theme name and definition are required");this.themes.set(e,t)}use(e){const t=this.resolvePluginAlias(e);this.resolvePlugin(t);const s=this.plugins.get(t);if(!s)throw new Error(`Unable to resolve "${t}" plugin, did you forget to register it?`);return s}create(e){return this.use(e)}resolveResponse(e){const t=Object.assign({envelopes:[],options:{},scripts:[],styles:[],context:{}},e);return Object.entries(t.options).forEach((([e,s])=>{t.options[e]=this.resolveOptions(s)})),t.context.csp_style_nonce=t.context.csp_style_nonce||"",t.context.csp_script_nonce=t.context.csp_script_nonce||"",t.envelopes.forEach((e=>{e.metadata=e.metadata||{},e.metadata.plugin=this.resolvePluginAlias(e.metadata.plugin),this.addThemeStyles(t,e.metadata.plugin),e.options=this.resolveOptions(e.options),e.context=t.context})),t}resolveOptions(e){if(!e)return{};const t=Object.assign({},e);return Object.entries(t).forEach((([e,s])=>{t[e]=this.resolveFunction(s)})),t}resolveFunction(e){var t,s,n,r;if("string"!=typeof e)return e;const o=e.match(/^function\s*(\w*)\s*\(([^)]*)\)\s*\{([\s\S]*)\}$/),i=e.match(/^\s*(\(([^)]*)\)|[^=]+)\s*=>\s*([\s\S]+)$/);if(!o&&!i)return e;let l,a;o?(l=null!==(s=null===(t=o[2])||void 0===t?void 0:t.split(",").map((e=>e.trim())).filter(Boolean))&&void 0!==s?s:[],a=o[3].trim()):(l=null!==(r=null===(n=i[2])||void 0===n?void 0:n.split(",").map((e=>e.trim())).filter(Boolean))&&void 0!==r?r:[],a=i[3].trim(),a=a.startsWith("{")?a.slice(1,-1).trim():`return ${a};`);try{return new Function(...l,a)}catch(t){return console.error("PHPFlasher: Error converting string to function:",t),e}}resolvePlugin(e){if(this.plugins.get(e)||!e.includes("theme."))return;const t=e.replace("theme.",""),n=this.themes.get(t);n&&this.addPlugin(e,new s(n))}resolvePluginAlias(e){return"flasher"===(e=e||this.defaultPlugin)?"theme.flasher":e}addAssets(t){return e(this,void 0,void 0,(function*(){try{const e=t.filter((e=>"style"===e.type)),s=[];for(const{urls:t,nonce:n,type:r}of e)if(null==t?void 0:t.length)for(const e of t)e&&!this.loadedAssets.has(e)&&(s.push(this.loadAsset(e,n,r)),this.loadedAssets.add(e));yield Promise.all(s);const n=t.filter((e=>"script"===e.type));for(const{urls:e,nonce:t,type:s}of n)if(null==e?void 0:e.length)for(const n of e)n&&!this.loadedAssets.has(n)&&(yield this.loadAsset(n,t,s),this.loadedAssets.add(n))}catch(e){console.error("PHPFlasher: Error loading assets",e)}}))}loadAsset(e,t,s){const n="style"===s?`link[href="${e}"]`:`script[src="${e}"]`;return document.querySelector(n)?Promise.resolve():new Promise(((n,r)=>{const o=document.createElement("style"===s?"link":"script");"style"===s?(o.rel="stylesheet",o.href=e):(o.type="text/javascript",o.src=e),t&&o.setAttribute("nonce",t),o.onload=()=>n(),o.onerror=()=>r(new Error(`Failed to load ${e}`)),document.head.appendChild(o)}))}addThemeStyles(e,t){if("flasher"!==t&&!t.includes("theme."))return;const s=t.replace("theme.",""),n=this.themes.get(s);if(!(null==n?void 0:n.styles))return;const r=Array.isArray(n.styles)?n.styles:[n.styles];e.styles=Array.from(new Set([...e.styles,...r]))}};return n.addTheme("flasher",{render:e=>{const{type:t,title:s,message:n}=e,r="error"===t||"warning"===t,o=r?"alert":"status",i=r?"assertive":"polite",l=s||t.charAt(0).toUpperCase()+t.slice(1);return`\n
\n
\n
\n
\n ${l}\n ${n}\n
\n \n
\n \n \n \n
`}}),"undefined"!=typeof window&&(window.flasher=n),n})); diff --git a/src/Toastr/Laravel/FlasherToastrServiceProvider.php b/src/Toastr/Laravel/FlasherToastrServiceProvider.php index f926db82..f646cd96 100644 --- a/src/Toastr/Laravel/FlasherToastrServiceProvider.php +++ b/src/Toastr/Laravel/FlasherToastrServiceProvider.php @@ -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; + }); + } } diff --git a/src/Toastr/Laravel/LivewireListener.php b/src/Toastr/Laravel/LivewireListener.php new file mode 100644 index 00000000..44b5838b --- /dev/null +++ b/src/Toastr/Laravel/LivewireListener.php @@ -0,0 +1,75 @@ +getPresenter()) { + return; + } + + $response = $event->getResponse() ?: ''; + if (!\is_string($response)) { + return; + } + + // Avoid duplicate script injection + if (false === strripos($response, ' +JAVASCRIPT; + + $event->setResponse($response); + } + + public function getSubscribedEvents(): string + { + return ResponseEvent::class; + } +} diff --git a/src/Toastr/Prime/Resources/assets/toastr.ts b/src/Toastr/Prime/Resources/assets/toastr.ts index 25225126..1e58ae85 100644 --- a/src/Toastr/Prime/Resources/assets/toastr.ts +++ b/src/Toastr/Prime/Resources/assets/toastr.ts @@ -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 diff --git a/src/Toastr/Prime/Resources/dist/flasher-toastr.esm.js b/src/Toastr/Prime/Resources/dist/flasher-toastr.esm.js index 3b7fb42f..dfdd33ea 100644 --- a/src/Toastr/Prime/Resources/dist/flasher-toastr.esm.js +++ b/src/Toastr/Prime/Resources/dist/flasher-toastr.esm.js @@ -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; diff --git a/src/Toastr/Prime/Resources/dist/flasher-toastr.js b/src/Toastr/Prime/Resources/dist/flasher-toastr.js index a9c3bfd3..624f8621 100644 --- a/src/Toastr/Prime/Resources/dist/flasher-toastr.js +++ b/src/Toastr/Prime/Resources/dist/flasher-toastr.js @@ -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; diff --git a/src/Toastr/Prime/Resources/dist/flasher-toastr.min.js b/src/Toastr/Prime/Resources/dist/flasher-toastr.min.js index 7b046b9a..2f15a268 100644 --- a/src/Toastr/Prime/Resources/dist/flasher-toastr.min.js +++ b/src/Toastr/Prime/Resources/dist/flasher-toastr.min.js @@ -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})); diff --git a/src/Toastr/Prime/Resources/dist/toastr.d.ts b/src/Toastr/Prime/Resources/dist/toastr.d.ts index 6ca27722..5106ea5a 100644 --- a/src/Toastr/Prime/Resources/dist/toastr.d.ts +++ b/src/Toastr/Prime/Resources/dist/toastr.d.ts @@ -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; } diff --git a/src/Toastr/Prime/Resources/public/flasher-toastr.min.js b/src/Toastr/Prime/Resources/public/flasher-toastr.min.js index 7b046b9a..2f15a268 100644 --- a/src/Toastr/Prime/Resources/public/flasher-toastr.min.js +++ b/src/Toastr/Prime/Resources/public/flasher-toastr.min.js @@ -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})); diff --git a/tests/Laravel/EventListener/ThemeLivewireListenerTest.php b/tests/Laravel/EventListener/ThemeLivewireListenerTest.php new file mode 100644 index 00000000..e6ee739a --- /dev/null +++ b/tests/Laravel/EventListener/ThemeLivewireListenerTest.php @@ -0,0 +1,70 @@ +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('No flasher', 'html'); + + ($this->listener)($event); + + $this->assertSame('No flasher', $event->getResponse()); + } + + public function testInvokeSkipsDuplicateInjection(): void + { + $response = ''; + $response .= ''; + + $event = new ResponseEvent($response, 'html'); + + ($this->listener)($event); + + $this->assertSame($response, $event->getResponse()); + } + + public function testInvokeInjectsLivewireScript(): void + { + $response = ''; + + $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()); + } +} diff --git a/tests/Noty/Laravel/FlasherNotyServiceProviderTest.php b/tests/Noty/Laravel/FlasherNotyServiceProviderTest.php index cc2475b5..3677a428 100644 --- a/tests/Noty/Laravel/FlasherNotyServiceProviderTest.php +++ b/tests/Noty/Laravel/FlasherNotyServiceProviderTest.php @@ -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(); diff --git a/tests/Noty/Laravel/LivewireListenerTest.php b/tests/Noty/Laravel/LivewireListenerTest.php new file mode 100644 index 00000000..832ad30b --- /dev/null +++ b/tests/Noty/Laravel/LivewireListenerTest.php @@ -0,0 +1,71 @@ +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('No flasher', 'html'); + + ($this->listener)($event); + + $this->assertSame('No flasher', $event->getResponse()); + } + + public function testInvokeSkipsDuplicateInjection(): void + { + $response = ''; + $response .= ''; + + $event = new ResponseEvent($response, 'html'); + + ($this->listener)($event); + + $this->assertSame($response, $event->getResponse()); + } + + public function testInvokeInjectsLivewireScript(): void + { + $response = ''; + + $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()); + } +} diff --git a/tests/Notyf/Laravel/FlasherNotyfServiceProviderTest.php b/tests/Notyf/Laravel/FlasherNotyfServiceProviderTest.php index 1179f85f..2c529c9f 100644 --- a/tests/Notyf/Laravel/FlasherNotyfServiceProviderTest.php +++ b/tests/Notyf/Laravel/FlasherNotyfServiceProviderTest.php @@ -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(); diff --git a/tests/Notyf/Laravel/LivewireListenerTest.php b/tests/Notyf/Laravel/LivewireListenerTest.php new file mode 100644 index 00000000..102395c1 --- /dev/null +++ b/tests/Notyf/Laravel/LivewireListenerTest.php @@ -0,0 +1,69 @@ +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('No flasher', 'html'); + + ($this->listener)($event); + + $this->assertSame('No flasher', $event->getResponse()); + } + + public function testInvokeSkipsDuplicateInjection(): void + { + $response = ''; + $response .= ''; + + $event = new ResponseEvent($response, 'html'); + + ($this->listener)($event); + + $this->assertSame($response, $event->getResponse()); + } + + public function testInvokeInjectsLivewireScript(): void + { + $response = ''; + + $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()); + } +} diff --git a/tests/Toastr/Laravel/FlasherToastrServiceProviderTest.php b/tests/Toastr/Laravel/FlasherToastrServiceProviderTest.php index 1d9289ef..8936edce 100644 --- a/tests/Toastr/Laravel/FlasherToastrServiceProviderTest.php +++ b/tests/Toastr/Laravel/FlasherToastrServiceProviderTest.php @@ -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(); diff --git a/tests/Toastr/Laravel/LivewireListenerTest.php b/tests/Toastr/Laravel/LivewireListenerTest.php new file mode 100644 index 00000000..bbe3ab73 --- /dev/null +++ b/tests/Toastr/Laravel/LivewireListenerTest.php @@ -0,0 +1,71 @@ +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('No flasher', 'html'); + + ($this->listener)($event); + + $this->assertSame('No flasher', $event->getResponse()); + } + + public function testInvokeSkipsDuplicateInjection(): void + { + $response = ''; + $response .= ''; + + $event = new ResponseEvent($response, 'html'); + + ($this->listener)($event); + + $this->assertSame($response, $event->getResponse()); + } + + public function testInvokeInjectsLivewireScript(): void + { + $response = ''; + + $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()); + } +} diff --git a/tests/adapters/noty.test.ts b/tests/adapters/noty.test.ts index 8b48c749..7a151864 100644 --- a/tests/adapters/noty.test.ts +++ b/tests/adapters/noty.test.ts @@ -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() + }) + }) }) diff --git a/tests/adapters/toastr.test.ts b/tests/adapters/toastr.test.ts index 7bf34828..c26b4021 100644 --- a/tests/adapters/toastr.test.ts +++ b/tests/adapters/toastr.test.ts @@ -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() + }) + }) }) diff --git a/tests/flasher-plugin.test.ts b/tests/flasher-plugin.test.ts index 3d228992..d435bb07 100644 --- a/tests/flasher-plugin.test.ts +++ b/tests/flasher-plugin.test.ts @@ -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) + }) + }) })