├── .eslintrc.js ├── .php-cs-fixer.dist.php ├── .stylelintrc ├── CODE_OF_CONDUCT.md ├── LICENSE.md ├── README-v1.md ├── README.md ├── babel.config.js ├── bootstrap └── app.php ├── builds └── cdn.js ├── composer.json ├── config └── tall-toasts.php ├── dist └── js │ ├── manifest.json │ ├── tall-toasts.js │ └── tall-toasts.js.map ├── package.json ├── phpcs.xml.dist ├── phpmd-ruleset.xml.dist ├── phpstan.neon.dist ├── resources ├── js │ └── tall-toasts.js └── views │ ├── includes │ ├── content.blade.php │ └── icon.blade.php │ └── livewire │ └── toasts.blade.php ├── rollup.config.js └── src ├── Concerns └── WireToast.php ├── Controllers ├── CanPretendToBeAFile.php └── JavaScriptAssets.php ├── Livewire └── ToastComponent.php ├── Notification.php ├── NotificationType.php ├── Toast.php ├── ToastBladeDirectives.php ├── ToastManager.php ├── ToastServiceProvider.php └── helpers.php /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | commonjs: true, 5 | es2021: true 6 | }, 7 | extends: [ 8 | 'standard' 9 | ], 10 | parser: '@typescript-eslint/parser', 11 | parserOptions: { 12 | ecmaVersion: 12 13 | }, 14 | plugins: [ 15 | '@typescript-eslint' 16 | ], 17 | rules: { 18 | semi: ['error', 'always'], 19 | quotes: [ 20 | 'error', 21 | 'single', 22 | { 23 | avoidEscape: true, 24 | allowTemplateLiterals: true 25 | } 26 | ] 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | in([ 5 | __DIR__ . '/src', 6 | __DIR__ . '/tests', 7 | ]) 8 | ->name('*.php') 9 | ->notName('*.blade.php') 10 | ->ignoreDotFiles(true) 11 | ->ignoreVCS(true); 12 | 13 | return (new PhpCsFixer\Config())->setRules([ 14 | '@PSR12' => true, 15 | 'array_syntax' => ['syntax' => 'short'], 16 | 'ordered_imports' => ['sort_algorithm' => 'alpha', 'imports_order' => ['const', 'class', 'function']], 17 | 'no_unused_imports' => true, 18 | 'not_operator_with_successor_space' => true, 19 | 'trailing_comma_in_multiline' => true, 20 | 'phpdoc_scalar' => true, 21 | 'unary_operator_spaces' => true, 22 | 'binary_operator_spaces' => true, 23 | 'blank_line_before_statement' => [ 24 | 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], 25 | ], 26 | 'phpdoc_single_line_var_spacing' => true, 27 | 'phpdoc_var_without_name' => true, 28 | 'class_attributes_separation' => [ 29 | 'elements' => [ 30 | 'method' => 'one', 31 | ], 32 | ], 33 | 'method_argument_space' => [ 34 | 'on_multiline' => 'ensure_fully_multiline', 35 | 'keep_multiple_spaces_after_comma' => true, 36 | ], 37 | 'single_trait_insert_per_statement' => true, 38 | ]) 39 | ->setFinder($finder); 40 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "at-rule-empty-line-before": [ 4 | "always", 5 | { 6 | "except": [ 7 | "blockless-after-same-name-blockless", 8 | "first-nested" 9 | ] 10 | } 11 | ], 12 | "at-rule-name-case": "lower", 13 | "at-rule-name-newline-after": "always-multi-line", 14 | "at-rule-name-space-after": "always-single-line", 15 | "at-rule-semicolon-space-before": "never", 16 | "block-no-empty": true, 17 | "color-no-invalid-hex": true, 18 | "comment-empty-line-before": [ 19 | "always", 20 | { 21 | "ignore": [ 22 | "after-comment" 23 | ] 24 | } 25 | ], 26 | "comment-no-empty": true, 27 | "comment-whitespace-inside": "always", 28 | "declaration-block-no-duplicate-properties": true, 29 | "declaration-block-no-redundant-longhand-properties": true, 30 | "declaration-colon-space-after": "always", 31 | "font-family-no-duplicate-names": true, 32 | "font-weight-notation": "numeric", 33 | "indentation": [ 34 | 2, 35 | { 36 | "ignore": [ 37 | "value" 38 | ] 39 | } 40 | ], 41 | "length-zero-no-unit": true, 42 | "max-empty-lines": 2, 43 | "max-line-length": 100, 44 | "max-nesting-depth": 3, 45 | "no-duplicate-selectors": true, 46 | "no-eol-whitespace": true, 47 | "no-extra-semicolons": true, 48 | "no-invalid-double-slash-comments": true, 49 | "no-missing-end-of-source-newline": true, 50 | "property-case": "lower", 51 | "property-no-unknown": true, 52 | "rule-empty-line-before": [ 53 | "always", 54 | { 55 | "except": [ 56 | "first-nested" 57 | ], 58 | "ignore": [ 59 | "after-comment" 60 | ] 61 | } 62 | ], 63 | "selector-list-comma-newline-after": "always", 64 | "selector-pseudo-class-no-unknown": true, 65 | "shorthand-property-no-redundant-values": true, 66 | "string-no-newline": true, 67 | "string-quotes": "double", 68 | "unit-case": "lower", 69 | "unit-no-unknown": true, 70 | "unit-whitelist": [ 71 | "deg", 72 | "em", 73 | "rem", 74 | "%", 75 | "ms", 76 | "s", 77 | "px", 78 | "vh", 79 | "vw" 80 | ], 81 | "value-keyword-case": "lower" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for 6 | everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity 7 | and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, 8 | or sexual identity and orientation. 9 | 10 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to a positive environment for our community include: 15 | 16 | - Demonstrating empathy and kindness toward other people 17 | - Being respectful of differing opinions, viewpoints, and experiences 18 | - Giving and gracefully accepting constructive feedback 19 | - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 20 | - Focusing on what is best not just for us as individuals, but for the overall community 21 | 22 | Examples of unacceptable behavior include: 23 | 24 | - The use of sexualized language or imagery, and sexual attention or advances of any kind 25 | - Trolling, insulting or derogatory comments, and personal or political attacks 26 | - Public or private harassment 27 | - Publishing others' private information, such as a physical or email address, without their explicit permission 28 | - Other conduct which could reasonably be considered inappropriate in a professional setting 29 | 30 | ## Enforcement Responsibilities 31 | 32 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take 33 | appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, 34 | or harmful. 35 | 36 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, 37 | issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for 38 | moderation decisions when appropriate. 39 | 40 | ## Scope 41 | 42 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing 43 | the community in public spaces. Examples of representing our community include using an official e-mail address, posting 44 | via an official social media account, or acting as an appointed representative at an online or offline event. 45 | 46 | ## Enforcement 47 | 48 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible 49 | for enforcement at . All complaints will be reviewed and investigated promptly and fairly. 50 | 51 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 52 | 53 | ## Enforcement Guidelines 54 | 55 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem 56 | in violation of this Code of Conduct: 57 | 58 | ### 1. Correction 59 | 60 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the 61 | community. 62 | 63 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation 64 | and an explanation of why the behavior was inappropriate. A public apology may be requested. 65 | 66 | ### 2. Warning 67 | 68 | **Community Impact**: A violation through a single incident or series of actions. 69 | 70 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including 71 | unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding 72 | interactions in community spaces as well as external channels like social media. Violating these terms may lead to a 73 | temporary or permanent ban. 74 | 75 | ### 3. Temporary Ban 76 | 77 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 78 | 79 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified 80 | period of time. No public or private interaction with the people involved, including unsolicited interaction with those 81 | enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 82 | 83 | ### 4. Permanent Ban 84 | 85 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate 86 | behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 87 | 88 | **Consequence**: A permanent ban from any sort of public interaction within the community. 89 | 90 | ## Attribution 91 | 92 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at 93 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 94 | 95 | Community Impact Guidelines were inspired 96 | by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 97 | 98 | For answers to common questions about this code of conduct, see the FAQ at 99 | https://www.contributor-covenant.org/faq. Translations are available at 100 | https://www.contributor-covenant.org/translations. 101 | 102 | [homepage]: https://www.contributor-covenant.org 103 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) @usernotnull 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README-v1.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # Beautiful Notification Toasts For Laravel 4 | 5 | A Toast global that can be called from the backend (via Controllers, Blade Views, Components) or frontend (JS, Alpine 6 | Components) to render customizable toasts. 7 | 8 | Runs with the TALL stack: [Laravel](https://laravel.com/docs/10.x/installation), 9 | [TailwindCSS](https://tailwindcss.com/docs/guides/laravel), 10 | [Livewire](https://laravel-livewire.com/docs/2.x/installation), 11 | [AlpineJS](https://alpinejs.dev/essentials/installation). 12 | 13 | [![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/usernotnull/tall-toasts?label=release&sort=semver&style=plastic)](https://github.com/usernotnull/tall-toasts/releases) 14 | [![Build size Brotli](https://img.badgesize.io/usernotnull/tall-toasts/main/dist/js/tall-toasts.js.svg?compression=brotli&style=plastic&color=green&label=JS%20size)](https://github.com/usernotnull/tall-toasts/blob/main/dist/js/tall-toasts.js) 15 | [![Scrutinizer Score](https://img.shields.io/scrutinizer/g/usernotnull/tall-toasts.svg?style=plastic&label=scrutinizer%20score)](https://scrutinizer-ci.com/g/usernotnull/tall-toasts) 16 | [![Codacy branch grade](https://img.shields.io/codacy/grade/0c6b4f96ac2a4a6cbf265f5e825a3fd2/main?style=plastic)](https://www.codacy.com/gh/usernotnull/tall-toasts/dashboard?utm_source=github.com&utm_medium=referral&utm_content=usernotnull/tall-toasts&utm_campaign=Badge_Grade) 17 | [![Codecov branch](https://img.shields.io/codecov/c/github/usernotnull/tall-toasts/main?style=plastic)](https://app.codecov.io/gh/usernotnull/tall-toasts) 18 | 19 | Light | Dark 20 | ------------ | ------------- 21 | ![toast-light](/.github/images/light.gif) | ![toast-dark](/.github/images/dark.gif) 22 | 23 |
24 | 25 | ## Featured On 26 | 27 | [Laravel News](https://laravel-news.com/toast-notifications-for-the-tall-stack)     [madewithlaravel](https://madewithlaravel.com/tall-toasts)     [Laravel News Podcast](https://podcasts.apple.com/us/podcast/laravel-news-podcast/id1051289963?mt=2) 28 | 29 | ## Why 30 | 31 | If you are building a web app with the TALL stack, you must choose this library over the other outdated libraries 32 | available: 33 | 34 | ### Size does matter 35 | 36 | Since the frontend is a pure AlpineJS component with no reliance on external JS libs, and since the backend handles most 37 | of the logic, the javascript footprint is 38 | tiny [(less than ONE kilobyte!)](https://img.badgesize.io/usernotnull/tall-toasts/main/dist/js/tall-toasts.js.svg?compression=brotli&style=plastic&color=green&label=JS%20size) 39 | . 40 | 41 | The CSS footprint is also negligible as it uses the default TailwindCSS classes. Even if you override the default views, 42 | you will rest assured that Tailwind's purging will only keep the styles/classes you have used. 43 | 44 | In plain English, it will not bloat your generated JS/CSS files nor add extra files to download as when using other JS 45 | libs! 46 | 47 | ### Takes advantage of all the niceties that come with TALL 48 | 49 | You can call it from anywhere! Memorize `Toast` for the frontend and `toast()` for the backend. 50 | 51 | See the [usage section](#usage) for examples. 52 | 53 | ### Customizable 54 | 55 | You have control over the view: As you are overriding the blade view, you'll be able to shape it as you like using 56 | TailwindCSS classes. 57 | 58 | No more messing with custom CSS overrides! 59 | 60 | ## Usage 61 | 62 | ### From The Frontend 63 | 64 | ```js 65 | Toast.info('Notification from the front-end...', 'The Title'); 66 | 67 | Toast.success('A toast without a title also works'); 68 | 69 | Toast.warning('Watch out!'); 70 | 71 | Toast.danger('I warned you!', 'Yikes'); 72 | 73 | Toast.debug('I will NOT show in production! Locally, I will also log in console...', 'A Debug Message'); 74 | 75 | Toast.success('This toast will display only for 3 seconds', 'The Title', 3000); 76 | 77 | Toast.success('This toast will display until you remove it manually', 'The Title', 0); 78 | ``` 79 | 80 | ### From The Backend 81 | 82 | ```php 83 | toast() 84 | ->info('I will appear only on the next page!') 85 | ->pushOnNextPage(); 86 | 87 | toast() 88 | ->info('Notification from the backend...', 'The Title') 89 | ->push(); 90 | 91 | toast() 92 | ->success('A toast without a title also works') 93 | ->push(); 94 | 95 | toast() 96 | ->warning('Watch out!') 97 | ->push(); 98 | 99 | toast() 100 | ->danger('I warned you!', 'Yikes') 101 | ->push(); 102 | 103 | toast() 104 | ->danger('I will go…
to the next line 💪', 'I am HOT') 105 | ->doNotSanitize() 106 | ->push(); 107 | 108 | toast() 109 | ->debug('I will NOT show in production! Locally, I will also log in console...', 'A Debug Message') 110 | ->push(); 111 | 112 | // debug also accepts objects as message 113 | toast() 114 | ->debug(User::factory()->createOne()->only(['name', 'email']), 'A User Dump') 115 | ->push(); 116 | 117 | toast() 118 | ->success('This toast will display only for 3 seconds') 119 | ->duration(3000) 120 | ->push(); 121 | 122 | toast() 123 | ->success('This toast will display until you remove it manually') 124 | ->sticky() 125 | ->push(); 126 | ``` 127 | 128 | You can call the above toast helper from controllers, blade views, and components. 129 | 130 | **To properly call it from inside livewire components, add the trait:** 131 | 132 | ```php 133 | use Livewire\Component; 134 | use Usernotnull\Toast\Concerns\WireToast; 135 | 136 | class DemoComponent extends Component 137 | { 138 | use WireToast; // <-- add this 139 | 140 | public function sendCookie(): void 141 | { 142 | toast() 143 | ->success('You earned a cookie! 🍪') 144 | ->pushOnNextPage(); 145 | 146 | redirect()->route('dashboard'); 147 | } 148 | ``` 149 | 150 | ## Support Me 151 | 152 | I plan on developing many open-source packages using the TALL stack. 153 | Consider supporting my work by tweeting about this library or by contributing to this package. 154 | 155 | Check out the list of other packages I built for the TALL stack [Other Packages](#other-packages). 156 | To stay updated, [follow me on Twitter](https://twitter.com/usernotnull). 157 | 158 | ## Requirements 159 | 160 | Dependency | Version 161 | ----|---- 162 | PHP | ^8.0 163 | Laravel | ^8.0 \| ^9.0 \| ^10.0 164 | TailwindCSS | ^2.0 \| ^3.0 165 | Livewire | ^2.0 166 | AlpineJS | ^3.0 167 | 168 | ## Installation 169 | 170 | You can install the package via [Composer](https://getcomposer.org/): 171 | 172 | ```bash 173 | composer require usernotnull/tall-toasts 174 | ``` 175 | 176 | ## Setup 177 | 178 | ### TailwindCSS 179 | 180 | Build your CSS as you usually do, ie 181 | 182 | ```bash 183 | npm run dev 184 | ``` 185 | 186 | #### Usage With Tailwind JIT 187 | 188 | If you are using [Just-in-Time Mode](https://tailwindcss.com/docs/just-in-time-mode), add these additional lines into 189 | your `tailwind.config.js` file: 190 | 191 | ```js 192 | // use `purge` instead of `content` if using TailwindCSS v2.x 193 | content: [ 194 | './vendor/usernotnull/tall-toasts/config/**/*.php', 195 | './vendor/usernotnull/tall-toasts/resources/views/**/*.blade.php', 196 | // etc... 197 | ] 198 | ``` 199 | 200 | This way, Tailwind JIT will include the classes used in this library in your CSS. 201 | 202 | *As usual, if the content of `tailwind.config.js` changes, you should re-run the npm command.* 203 | 204 | ### Registering Toast with AlpineJS 205 | 206 | Next, you need to register `Toast` with AlpineJS. How this is done depends on which method you used to add Alpine to your project: 207 | 208 | #### AlpineJS installed as an NPM Module 209 | 210 | If you have installed AlpineJS through NPM, you can add the Toast component by changing your `app.js` file to match: 211 | 212 | ```js 213 | import Alpine from "alpinejs" 214 | import ToastComponent from '../../vendor/usernotnull/tall-toasts/resources/js/tall-toasts' 215 | 216 | Alpine.data('ToastComponent', ToastComponent) 217 | 218 | window.Alpine = Alpine 219 | Alpine.start() 220 | ``` 221 | 222 | *If you have a custom directory structure, you may have to adjust the above import path until it correctly points 223 | to `tall-toasts.js` inside this vendor file.* 224 | 225 | Include the `@toastScripts` blade directive *BEFORE* the `mix()` helper if using Laravel Mix, if using Vite, include it before the `@vite` blade directive. 226 | 227 | ```html 228 | @toastScripts 229 | 230 | <--- Vite ---> 231 | @vite(['resources/css/app.css', 'resources/js/app.js']) 232 | 233 | <--- Mix ---> 234 | 235 | ``` 236 | 237 | #### AlpineJS added via script tag 238 | 239 | If you imported AlpineJS via a script tag simply add the `@toastScripts` blade directive *BEFORE* importing AlpineJS: 240 | 241 | ```html 242 | @toastScripts 243 | 244 | ``` 245 | 246 | ### The View 247 | 248 | Add `` **as high as possible** in the body tag, ie: 249 | 250 | ```html 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | ``` 266 | 267 | That's it! 🎉 268 | 269 | *** 270 | 271 | ## RTL Support 272 | 273 | The default layout now supports RTL. 274 | 275 | As per TailwindCSS docs on [RTL support](https://tailwindcss.com/docs/hover-focus-and-other-states#rtl-support): 276 | `Always set the direction, even if left-to-right is your default`. 277 | 278 | ```html 279 | 280 | 281 | 282 | ``` 283 | 284 | ## Customization 285 | 286 | The toasts should look pretty good out of the box. However, we've documented a couple of ways to customize the views and 287 | functionality. 288 | 289 | ### Configuration 290 | 291 | You can publish the config file with: 292 | 293 | ```bash 294 | php artisan vendor:publish --provider="Usernotnull\Toast\ToastServiceProvider" --tag="tall-toasts-config" 295 | ``` 296 | 297 | These are the default contents of the published config file: 298 | 299 | ```php 300 | 5000, 307 | 308 | /* 309 | * How long to wait before displaying the toasts after page loads, in ms 310 | */ 311 | 'load_delay' => 400, 312 | ]; 313 | 314 | ``` 315 | 316 | ### Customizing views 317 | 318 | You can publish and change all views in this package: 319 | 320 | ```bash 321 | php artisan vendor:publish --provider="Usernotnull\Toast\ToastServiceProvider" --tag="tall-toasts-views" 322 | ``` 323 | 324 | The published views can be found and changed in `resources/views/vendor/tall-toast/`. 325 | 326 | The published files are: 327 | 328 | - `includes/content.blade.php` - *the content view of each popup notification, fully configurable* 329 | - `includes/icon.blade.php` - *the icons of each notification type* 330 | - `livewire/toasts.blade.php` - *the parent of all toasts* 331 | 332 | #### Text Sanitization 333 | 334 | The content view displays the title and message with x-html. This is fine since the backend sanitizes the title and 335 | message by default. 336 | 337 | ⚠️ If you wish to skip sanitization in order to display HTML content, such as bolding the text or adding `
` to go to 338 | the next line, you will call doNotSanitize() as seen in the [usage section](#usage). In such case, make sure no user 339 | input is provided! 340 | 341 | ## Troubleshooting 342 | 343 | Make sure you thoroughly go through this readme first! 344 | 345 | If the checklist below does not resolve your problem, feel free 346 | to [submit an issue](https://github.com/usernotnull/tall-toasts/issues/new/choose). Make sure to follow the bug report 347 | template. It helps us quickly reproduce the bug and resolve it. 348 | 349 | ### The toasts show multiple times only after refresh 350 | 351 | - If you are calling toasts from a livewire component, 352 | did you add the trait WireToast to the component? [(see)](#from-the-backend) 353 | 354 | - Did you swap push() and pushOnNextPage()? 355 | 356 | ### The toasts won't show 357 | 358 | - Is the located in a page that has both the livewire and alpine/app.js script tags 359 | inserted? [(see)](#the-view) 360 | 361 | - Did you skip adding the ToastComponent as an alpine data component? [(see)](#alpinejs) 362 | 363 | - Did you forget calling push() at the end of the chained method? [(see)](#usage) 364 | 365 | - Have you tried calling the toast() helper function from another part of the application and check if it worked (it 366 | will help us scope the problem)? [(see)](#usage) 367 | 368 | - Did you try calling `php artisan view:clear`? 369 | 370 | - Are you getting any console errors? 371 | 372 | ### The toasts show but look weird 373 | 374 | - Are you using TailwindCSS JIT? Don't forget to update your purge list! [(see)](#usage-with-tailwind-jit) 375 | - You may need to rebuild your CSS by running: `npm run dev` or re-running `npm run watch` [(see)](#tailwindcss) 376 | 377 | ## Other Packages 378 | 379 | To stay updated, [follow me on Twitter](https://twitter.com/usernotnull). 380 | 381 | ## Testing 382 | 383 | This package uses [PestPHP](https://pestphp.com/) to run its tests. 384 | 385 | - To run tests without coverage, run: 386 | 387 | ```bash 388 | composer test 389 | ``` 390 | 391 | - To run tests with coverage, run: 392 | 393 | ```bash 394 | composer test-coverage 395 | ``` 396 | 397 | ## Contributing 398 | 399 | This package has 3 GitHub Workflows which run sequentially when pushing PRs: 400 | 401 | - First, it checks for styling issues and automatically fixes them using: 402 | 1. [PHP CS Fixer](https://github.com/FriendsOfPHP/PHP-CS-Fixer) 403 | 2. [PHP Code Beautifier](https://github.com/squizlabs/PHP_CodeSniffer) 404 | 405 | - Then, it uses static analysis followed by standard unit tests using: 406 | 1. [Psalm](https://psalm.dev/) 407 | 2. [PHP Stan](https://github.com/phpstan/phpstan) 408 | 3. [PHP MessDetector](https://phpmd.org/) 409 | 4. [PHP Code Sniffer](https://github.com/squizlabs/PHP_CodeSniffer) 410 | 5. [PestPHP](https://pestphp.com/) 411 | 412 | - Finally, it generates the minified JS dist which is injected by @toastScripts 413 | 414 | When pushing PRs, it's a good idea to do a quick run and make sure the workflow checks out, which saves time during code 415 | review before merging. 416 | 417 | To facilitate the job, you can run the below command before pushing the PR: 418 | 419 | ```bash 420 | composer workflow 421 | ``` 422 | 423 | Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. 424 | 425 | ## Changelog 426 | 427 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 428 | 429 | ## Versioning 430 | 431 | This project follows the [Semantic Versioning](https://semver.org/) guidelines. 432 | 433 | ## Security Vulnerabilities 434 | 435 | As per security best practices, do not call `doNotSanitize()` on a toast that has user input in its message or title! 436 | 437 | Please review [the security policy](https://github.com/usernotnull/tall-toasts/security/policy) on how to report 438 | security vulnerabilities. 439 | 440 | ## Credits 441 | 442 | - [John F](https://github.com/usernotnull) ( [@usernotnull](https://twitter.com/usernotnull) ) 443 | - [All Contributors](../../contributors) 444 | 445 | ## License 446 | 447 | The MIT License (MIT). Please see [the license file](LICENSE.md) for more information. 448 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # Beautiful Notification Toasts For Laravel & Livewire 4 | 5 | A Toast global that can be called from the backend (via Controllers, Blade Views, Components) or frontend (JS, Alpine 6 | Components) to render customizable toasts. 7 | 8 | Runs with the TALL stack: [Laravel](https://laravel.com/docs/10.x/installation), 9 | [TailwindCSS](https://tailwindcss.com/docs/guides/laravel), 10 | [Livewire](https://laravel-livewire.com/docs/2.x/installation), 11 | [AlpineJS](https://alpinejs.dev/essentials/installation). 12 | 13 | [![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/usernotnull/tall-toasts?label=release&sort=semver&style=plastic)](https://github.com/usernotnull/tall-toasts/releases) 14 | [![Build size Brotli](https://img.badgesize.io/usernotnull/tall-toasts/main/dist/js/tall-toasts.js.svg?compression=brotli&style=plastic&color=green&label=JS%20size)](https://github.com/usernotnull/tall-toasts/blob/main/dist/js/tall-toasts.js) 15 | [![Scrutinizer Score](https://img.shields.io/scrutinizer/g/usernotnull/tall-toasts.svg?style=plastic&label=scrutinizer%20score)](https://scrutinizer-ci.com/g/usernotnull/tall-toasts) 16 | [![Codacy branch grade](https://img.shields.io/codacy/grade/0c6b4f96ac2a4a6cbf265f5e825a3fd2/main?style=plastic)](https://www.codacy.com/gh/usernotnull/tall-toasts/dashboard?utm_source=github.com&utm_medium=referral&utm_content=usernotnull/tall-toasts&utm_campaign=Badge_Grade) 17 | [![Codecov branch](https://img.shields.io/codecov/c/github/usernotnull/tall-toasts/main?style=plastic)](https://app.codecov.io/gh/usernotnull/tall-toasts) 18 | 19 | Light | Dark 20 | ------------ | ------------- 21 | ![toast-light](/.github/images/light.gif) | ![toast-dark](/.github/images/dark.gif) 22 | 23 |
24 | 25 | ## Featured On 26 | 27 | [Laravel News](https://laravel-news.com/toast-notifications-for-the-tall-stack)     [madewithlaravel](https://madewithlaravel.com/tall-toasts)     [Laravel News Podcast](https://podcasts.apple.com/us/podcast/laravel-news-podcast/id1051289963?mt=2) 28 | 29 | ## Why 30 | 31 | If you are building a web app with the TALL stack, you must choose this library over the other outdated libraries 32 | available: 33 | 34 | ### Size does matter 35 | 36 | Since the frontend is a pure AlpineJS component with no reliance on external JS libs, and since the backend handles most 37 | of the logic, the javascript footprint is 38 | tiny [(less than ONE kilobyte!)](https://img.badgesize.io/usernotnull/tall-toasts/main/dist/js/tall-toasts.js.svg?compression=brotli&style=plastic&color=green&label=JS%20size) 39 | . 40 | 41 | The CSS footprint is also negligible as it uses the default TailwindCSS classes. Even if you override the default views, 42 | you will rest assured that Tailwind's purging will only keep the styles/classes you have used. 43 | 44 | In plain English, it will not bloat your generated JS/CSS files nor add extra files to download as when using other JS 45 | libs! 46 | 47 | ### Takes advantage of all the niceties that come with TALL 48 | 49 | You can call it from anywhere! Memorize `Toast` for the frontend and `toast()` for the backend. 50 | 51 | See the [usage section](#usage) for examples. 52 | 53 | ### Customizable 54 | 55 | You have control over the view: As you are overriding the blade view, you'll be able to shape it as you like using 56 | TailwindCSS classes. 57 | 58 | No more messing with custom CSS overrides! 59 | 60 | ## Usage 61 | 62 | ### From The Frontend 63 | 64 | ```js 65 | Toast.info('Notification from the front-end...', 'The Title'); 66 | 67 | Toast.success('A toast without a title also works'); 68 | 69 | Toast.warning('Watch out!'); 70 | 71 | Toast.danger('I warned you!', 'Yikes'); 72 | 73 | Toast.debug('I will NOT show in production! Locally, I will also log in console...', 'A Debug Message'); 74 | 75 | Toast.success('This toast will display only for 3 seconds', 'The Title', 3000); 76 | 77 | Toast.success('This toast will display until you remove it manually', 'The Title', 0); 78 | ``` 79 | 80 | ### From The Backend 81 | 82 | ```php 83 | toast() 84 | ->info('I will appear only on the next page!') 85 | ->pushOnNextPage(); 86 | 87 | toast() 88 | ->info('Notification from the backend...', 'The Title') 89 | ->push(); 90 | 91 | toast() 92 | ->success('A toast without a title also works') 93 | ->push(); 94 | 95 | toast() 96 | ->warning('Watch out!') 97 | ->push(); 98 | 99 | toast() 100 | ->danger('I warned you!', 'Yikes') 101 | ->push(); 102 | 103 | toast() 104 | ->danger('I will go…
to the next line 💪', 'I am HOT') 105 | ->doNotSanitize() 106 | ->push(); 107 | 108 | toast() 109 | ->debug('I will NOT show in production! Locally, I will also log in console...', 'A Debug Message') 110 | ->push(); 111 | 112 | // debug also accepts objects as message 113 | toast() 114 | ->debug(User::factory()->createOne()->only(['name', 'email']), 'A User Dump') 115 | ->push(); 116 | 117 | toast() 118 | ->success('This toast will display only for 3 seconds') 119 | ->duration(3000) 120 | ->push(); 121 | 122 | toast() 123 | ->success('This toast will display until you remove it manually') 124 | ->sticky() 125 | ->push(); 126 | ``` 127 | 128 | You can call the above toast helper from controllers, blade views, and components. 129 | 130 | ## Support Me 131 | 132 | I plan on developing many open-source packages using the TALL stack. 133 | Consider supporting my work by tweeting about this library or by contributing to this package. 134 | 135 | Check out the list of other packages I built for the TALL stack [Other Packages](#other-packages). 136 | To stay updated, [follow me on Twitter](https://twitter.com/usernotnull). 137 | 138 | ## Requirements 139 | 140 | Dependency | Version 141 | ----|---- 142 | PHP | ^8.0 143 | Laravel | ^8.0 \| ^9.0 \| ^10.0 \| ^11.0 \| ^12.0 144 | TailwindCSS | ^2.0 \| ^3.0 145 | Livewire | ^2.0 \| ^3.0 (as of tall-toasts v2) 146 | AlpineJS | ^3.0 147 | 148 | You can find the older [v1 documentation here](README-v1.md) 149 | 150 | ## Installation 151 | 152 | You can install the package via [Composer](https://getcomposer.org/): 153 | 154 | ```bash 155 | composer require usernotnull/tall-toasts 156 | ``` 157 | 158 | ## Setup 159 | 160 | ### TailwindCSS 161 | 162 | Build your CSS as you usually do, ie 163 | 164 | ```bash 165 | npm run dev 166 | ``` 167 | 168 | #### Usage With Tailwind JIT 169 | 170 | If you are using [Just-in-Time Mode](https://tailwindcss.com/docs/just-in-time-mode), add these additional lines into 171 | your `tailwind.config.js` file: 172 | 173 | ```js 174 | // use `purge` instead of `content` if using TailwindCSS v2.x 175 | content: [ 176 | './vendor/usernotnull/tall-toasts/config/**/*.php', 177 | './vendor/usernotnull/tall-toasts/resources/views/**/*.blade.php', 178 | // etc... 179 | ] 180 | ``` 181 | 182 | This way, Tailwind JIT will include the classes used in this library in your CSS. 183 | 184 | *As usual, if the content of `tailwind.config.js` changes, you should re-run the npm command.* 185 | 186 | ### Registering Toast with AlpineJS 187 | 188 | Add the Toast component in your `app.js`: 189 | 190 | ```js 191 | import {Alpine, Livewire} from '../../vendor/livewire/livewire/dist/livewire.esm'; 192 | import ToastComponent from '../../vendor/usernotnull/tall-toasts/resources/js/tall-toasts' 193 | 194 | Alpine.plugin(ToastComponent) 195 | 196 | Livewire.start() 197 | ``` 198 | 199 | Add `` **as high as possible** in the body tag, ie: 200 | 201 | ```html 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | @livewireScriptConfig 215 | 216 | 217 | 218 | ``` 219 | 220 | To properly dispatch toasts from inside your livewire components, **add the trait**: 221 | 222 | ```php 223 | use Livewire\Component; 224 | use Usernotnull\Toast\Concerns\WireToast; 225 | 226 | class DemoComponent extends Component 227 | { 228 | use WireToast; // <-- add this 229 | 230 | public function sendCookie(): void 231 | { 232 | toast() 233 | ->success('You earned a cookie! 🍪') 234 | ->pushOnNextPage(); 235 | 236 | redirect()->route('dashboard'); 237 | } 238 | ``` 239 | 240 | That's it! 🎉 241 | 242 | ## RTL Support 243 | 244 | The default layout now supports RTL. 245 | 246 | As per TailwindCSS docs on [RTL support](https://tailwindcss.com/docs/hover-focus-and-other-states#rtl-support): 247 | `Always set the direction, even if left-to-right is your default`. 248 | 249 | ```html 250 | 251 | 252 | 253 | ``` 254 | 255 | ## Customization 256 | 257 | The toasts should look pretty good out of the box. However, we've documented a couple of ways to customize the views and 258 | functionality. 259 | 260 | ### Configuration 261 | 262 | You can publish the config file with: 263 | 264 | ```bash 265 | php artisan vendor:publish --provider="Usernotnull\Toast\ToastServiceProvider" --tag="tall-toasts-config" 266 | ``` 267 | 268 | These are the default contents of the published config file: 269 | 270 | ```php 271 | 5000, 278 | 279 | /* 280 | * How long to wait before displaying the toasts after page loads, in ms 281 | */ 282 | 'load_delay' => 400, 283 | ]; 284 | 285 | ``` 286 | 287 | ### Customizing views 288 | 289 | You can publish and change all views in this package: 290 | 291 | ```bash 292 | php artisan vendor:publish --provider="Usernotnull\Toast\ToastServiceProvider" --tag="tall-toasts-views" 293 | ``` 294 | 295 | The published views can be found and changed in `resources/views/vendor/tall-toast/`. 296 | 297 | The published files are: 298 | 299 | - `includes/content.blade.php` - *the content view of each popup notification, fully configurable* 300 | - `includes/icon.blade.php` - *the icons of each notification type* 301 | - `livewire/toasts.blade.php` - *the parent of all toasts* 302 | 303 | #### Text Sanitization 304 | 305 | The content view displays the title and message with x-html. This is fine since the backend sanitizes the title and 306 | message by default. 307 | 308 | ⚠️ If you wish to skip sanitization in order to display HTML content, such as bolding the text or adding `
` to go to 309 | the next line, you will call doNotSanitize() as seen in the [usage section](#usage). In such case, make sure no user 310 | input is provided! 311 | 312 | ## Troubleshooting 313 | 314 | Make sure you thoroughly go through this readme first! 315 | 316 | If the checklist below does not resolve your problem, feel free 317 | to [submit an issue](https://github.com/usernotnull/tall-toasts/issues/new/choose). Make sure to follow the bug report 318 | template. It helps us quickly reproduce the bug and resolve it. 319 | 320 | ### The toasts show multiple times only after refresh 321 | 322 | - If you are calling toasts from a livewire component, 323 | did you add the trait WireToast to the component? [(see)](#from-the-backend) 324 | 325 | - Did you swap push() and pushOnNextPage()? 326 | 327 | ### The toasts won't show 328 | 329 | - Is the located in a page that has both the livewire and alpine/app.js script tags 330 | inserted? 331 | 332 | - Did you skip adding the ToastComponent as an alpine data component? 333 | 334 | - Did you forget calling push() at the end of the chained method? [(see)](#usage) 335 | 336 | - Have you tried calling the toast() helper function from another part of the application and check if it worked (it 337 | will help us scope the problem)? [(see)](#usage) 338 | 339 | - Did you try calling `php artisan view:clear`? 340 | 341 | - Are you getting any console errors? 342 | 343 | ### The toasts show but look weird 344 | 345 | - Are you using TailwindCSS JIT? Don't forget to update your purge list! [(see)](#usage-with-tailwind-jit) 346 | - You may need to rebuild your CSS by running: `npm run dev` or re-running `npm run watch` [(see)](#tailwindcss) 347 | 348 | ## Other Packages 349 | 350 | To stay updated, [follow me on Twitter](https://twitter.com/usernotnull). 351 | 352 | ## Testing 353 | 354 | This package uses [PestPHP](https://pestphp.com/) to run its tests. 355 | 356 | - To run tests without coverage, run: 357 | 358 | ```bash 359 | composer test 360 | ``` 361 | 362 | - To run tests with coverage, run: 363 | 364 | ```bash 365 | composer test-coverage 366 | ``` 367 | 368 | ## Contributing 369 | 370 | This package has 3 GitHub Workflows which run sequentially when pushing PRs: 371 | 372 | - First, it checks for styling issues and automatically fixes them using: 373 | 1. [PHP CS Fixer](https://github.com/FriendsOfPHP/PHP-CS-Fixer) 374 | 2. [PHP Code Beautifier](https://github.com/squizlabs/PHP_CodeSniffer) 375 | 376 | - Then, it uses static analysis followed by standard unit tests using: 377 | 1. [Psalm](https://psalm.dev/) 378 | 2. [PHP Stan](https://github.com/phpstan/phpstan) 379 | 3. [PHP MessDetector](https://phpmd.org/) 380 | 4. [PHP Code Sniffer](https://github.com/squizlabs/PHP_CodeSniffer) 381 | 5. [PestPHP](https://pestphp.com/) 382 | 383 | - Finally, it generates the minified JS dist which is injected by @toastScripts 384 | 385 | When pushing PRs, it's a good idea to do a quick run and make sure the workflow checks out, which saves time during code 386 | review before merging. 387 | 388 | To facilitate the job, you can run the below command before pushing the PR: 389 | 390 | ```bash 391 | composer workflow 392 | ``` 393 | 394 | Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. 395 | 396 | ## Changelog 397 | 398 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 399 | 400 | ## Versioning 401 | 402 | This project follows the [Semantic Versioning](https://semver.org/) guidelines. 403 | 404 | ## Security Vulnerabilities 405 | 406 | As per security best practices, do not call `doNotSanitize()` on a toast that has user input in its message or title! 407 | 408 | Please review [the security policy](https://github.com/usernotnull/tall-toasts/security/policy) on how to report 409 | security vulnerabilities. 410 | 411 | ## Credits 412 | 413 | - [John F](https://github.com/usernotnull) ( [@usernotnull](https://twitter.com/usernotnull) ) 414 | - [All Contributors](../../contributors) 415 | 416 | ## License 417 | 418 | The MIT License (MIT). Please see [the license file](LICENSE.md) for more information. 419 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current', 8 | edge: '18', 9 | ie: '11' 10 | } 11 | } 12 | ] 13 | ], 14 | plugins: [ 15 | '@babel/plugin-proposal-object-rest-spread' 16 | ], 17 | env: { 18 | test: { 19 | presets: [ 20 | [ 21 | '@babel/preset-env', 22 | { 23 | targets: { 24 | node: 'current' 25 | } 26 | } 27 | ] 28 | ] 29 | } 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /bootstrap/app.php: -------------------------------------------------------------------------------- 1 | { 4 | toast(window.Alpine); 5 | }); 6 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "usernotnull/tall-toasts", 3 | "type": "library", 4 | "description": "A Toast notification library for the Laravel TALL stack. You can push notifications from the backend or frontend to render customizable toasts with almost zero footprint on the published CSS/JS!", 5 | "keywords": [ 6 | "usernotnull", 7 | "tall-toasts", 8 | "toast-notifications", 9 | "ToastManager", 10 | "ui-components", 11 | "laravel-package", 12 | "laravel", 13 | "livewire", 14 | "tailwindcss", 15 | "alpinejs", 16 | "tall-stack" 17 | ], 18 | "homepage": "https://github.com/usernotnull/tall-toasts", 19 | "license": "MIT", 20 | "authors": [ 21 | { 22 | "name": "John F (@usernotnull)", 23 | "email": "15612814+usernotnull@users.noreply.github.com", 24 | "role": "Developer" 25 | } 26 | ], 27 | "require": { 28 | "php": "^8.0|^8.1", 29 | "illuminate/contracts": "^8.15 || 9.0 - 9.34 || ^9.36 || ^10.0|^11.0|^12.0", 30 | "spatie/laravel-package-tools": "^1.19", 31 | "livewire/livewire": "^v3" 32 | }, 33 | "require-dev": { 34 | "pestphp/pest": "^1.23.1|^3.7", 35 | "nunomaduro/larastan": "^2.9", 36 | "laravel/pint": "^1.21", 37 | "nunomaduro/collision": "^6.4.0", 38 | "orchestra/testbench": "^8.23.2|^10.0", 39 | "pestphp/pest-plugin-laravel": "^1.4.0|^3.1", 40 | "friendsofphp/php-cs-fixer": "^v3.71", 41 | "pestphp/pest-plugin-parallel": "^1.2", 42 | "phpmd/phpmd": "^2.15", 43 | "squizlabs/php_codesniffer": "^3.11", 44 | "vimeo/psalm": "^5.26.1|^6.6" 45 | }, 46 | "autoload": { 47 | "files": [ 48 | "src/helpers.php" 49 | ], 50 | "psr-4": { 51 | "Usernotnull\\Toast\\": "src" 52 | } 53 | }, 54 | "autoload-dev": { 55 | "psr-4": { 56 | "Usernotnull\\Toast\\Tests\\": "tests" 57 | } 58 | }, 59 | "scripts": { 60 | "test": "./vendor/bin/pest --no-coverage", 61 | "workflow": "./vendor/bin/php-cs-fixer fix && ./vendor/bin/phpcs && ./vendor/bin/phpcbf && ./vendor/bin/psalm --output-format=github && ./vendor/bin/phpstan && ./vendor/bin/phpmd src github phpmd-ruleset.xml.dist && XDEBUG_MODE=coverage vendor/bin/pest --parallel --coverage --min=100", 62 | "test-coverage": "XDEBUG_MODE=coverage ./vendor/bin/pest --parallel --coverage --min=100" 63 | }, 64 | "config": { 65 | "sort-packages": true, 66 | "allow-plugins": { 67 | "pestphp/pest-plugin": true, 68 | "composer/package-versions-deprecated": true 69 | } 70 | }, 71 | "extra": { 72 | "laravel": { 73 | "providers": [ 74 | "Usernotnull\\Toast\\ToastServiceProvider" 75 | ], 76 | "aliases": { 77 | "Toast": "ToastManager" 78 | } 79 | } 80 | }, 81 | "minimum-stability": "dev", 82 | "prefer-stable": true 83 | } 84 | -------------------------------------------------------------------------------- /config/tall-toasts.php: -------------------------------------------------------------------------------- 1 | 5000, 8 | 9 | /* 10 | * How long to wait before displaying the toasts after page loads, in ms 11 | */ 12 | 'load_delay' => 400, 13 | 14 | /* 15 | * Session keys used. 16 | * No need to edit unless the keys are already being used and conflict. 17 | */ 18 | 'session_keys' => [ 19 | 'toasts' => 'toasts', 20 | 'toasts_next_page' => 'toasts-next', 21 | ], 22 | ]; 23 | -------------------------------------------------------------------------------- /dist/js/manifest.json: -------------------------------------------------------------------------------- 1 | {"/tall-toasts.js":"/tall-toasts.js?id=8cebbd1d7ec8bfb4c7d2"} -------------------------------------------------------------------------------- /dist/js/tall-toasts.js: -------------------------------------------------------------------------------- 1 | !function(factory){"function"==typeof define&&define.amd?define(factory):factory()}((function(){"use strict";document.addEventListener("alpine:initializing",(function(){window.Alpine.data("ToastComponent",(function($wire){return{defaultDuration:$wire.defaultDuration,wireToasts:$wire.$entangle("toasts"),prod:$wire.$entangle("prod"),wireToastsIndex:0,toasts:[],pendingToasts:[],pendingRemovals:[],count:0,loaded:!1,init:function(){var _this=this;window.Toast={component:this,make:function(message,title,type,duration){return{title:title,message:message,type:type,duration:duration}},debug:function(message){var title=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",duration=arguments.length>2&&void 0!==arguments[2]?arguments[2]:void 0;this.component.add(this.make(message,title,"debug",null!=duration?duration:this.component.defaultDuration))},info:function(message){var title=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",duration=arguments.length>2&&void 0!==arguments[2]?arguments[2]:void 0;this.component.add(this.make(message,title,"info",null!=duration?duration:this.component.defaultDuration))},success:function(message){var title=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",duration=arguments.length>2&&void 0!==arguments[2]?arguments[2]:void 0;this.component.add(this.make(message,title,"success",null!=duration?duration:this.component.defaultDuration))},warning:function(message){var title=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",duration=arguments.length>2&&void 0!==arguments[2]?arguments[2]:void 0;this.component.add(this.make(message,title,"warning",null!=duration?duration:this.component.defaultDuration))},danger:function(message){var title=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",duration=arguments.length>2&&void 0!==arguments[2]?arguments[2]:void 0;this.component.add(this.make(message,title,"danger",null!=duration?duration:this.component.defaultDuration))}},addEventListener("toast",(function(event){_this.add(event.detail)})),this.fetchWireToasts(),this.$watch("wireToasts",(function(){_this.fetchWireToasts()})),setTimeout((function(){_this.loaded=!0,_this.pendingToasts.forEach((function(toast){_this.add(toast)})),_this.pendingToasts=null}),$wire.loadDelay)},fetchWireToasts:function(){var _this2=this;this.wireToasts.forEach((function(toast,i){i<_this2.wireToastsIndex||(_this2.add(window.Alpine.raw(toast)),_this2.wireToastsIndex++)}))},add:function(toast){var _toast$type;if(!0===this.loaded){if("debug"===toast.type){if(this.prod)return;console.log(toast.title,toast.message)}null!==(_toast$type=toast.type)&&void 0!==_toast$type||(toast.type="info"),toast.show=0,toast.index=this.count,this.toasts[this.count]=toast,this.scheduleRemoval(this.count),this.count++}else this.pendingToasts.push(toast)},scheduleRemoval:function(toastIndex){var _this3=this;Object.keys(this.pendingRemovals).includes(toastIndex.toString())||0!==this.toasts[toastIndex].duration&&(this.pendingRemovals[toastIndex]=setTimeout((function(){_this3.remove(toastIndex)}),this.toasts[toastIndex].duration))},scheduleRemovalWithOlder:function(){for(var toastIndex=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.count,i=0;i=toastIndex;i--)clearTimeout(this.pendingRemovals[i]),delete this.pendingRemovals[i]},remove:function(index){var _this4=this;this.toasts[index]&&(this.toasts[index].show=0),setTimeout((function(){_this4.toasts[index]="",delete _this4.pendingRemovals[index]}),500)}}}))}))})); 2 | //# sourceMappingURL=tall-toasts.js.map 3 | -------------------------------------------------------------------------------- /dist/js/tall-toasts.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"tall-toasts.js","sources":["../../builds/cdn.js","../../resources/js/tall-toasts.js"],"sourcesContent":["import toast from '../resources/js/tall-toasts';\n\ndocument.addEventListener('alpine:initializing', () => {\n toast(window.Alpine);\n});\n","export default function (Alpine) {\n Alpine.data('ToastComponent',\n ($wire) => ({\n defaultDuration: $wire.defaultDuration,\n wireToasts: $wire.$entangle('toasts'),\n prod: $wire.$entangle('prod'),\n wireToastsIndex: 0,\n toasts: [],\n pendingToasts: [],\n pendingRemovals: [],\n count: 0,\n loaded: false,\n\n init () {\n window.Toast = {\n component: this,\n\n make: (message, title, type, duration) => ({ title, message, type, duration }),\n\n debug (message, title = '', duration = undefined) {\n this.component.add(this.make(message, title, 'debug', duration ?? this.component.defaultDuration));\n },\n\n info (message, title = '', duration = undefined) {\n this.component.add(this.make(message, title, 'info', duration ?? this.component.defaultDuration));\n },\n\n success (message, title = '', duration = undefined) {\n this.component.add(this.make(message, title, 'success', duration ?? this.component.defaultDuration));\n },\n\n warning (message, title = '', duration = undefined) {\n this.component.add(this.make(message, title, 'warning', duration ?? this.component.defaultDuration));\n },\n\n danger (message, title = '', duration = undefined) {\n this.component.add(this.make(message, title, 'danger', duration ?? this.component.defaultDuration));\n }\n };\n\n addEventListener('toast', (event) => {\n this.add(event.detail);\n });\n\n this.fetchWireToasts();\n\n this.$watch('wireToasts', () => {\n this.fetchWireToasts();\n });\n\n setTimeout(() => {\n this.loaded = true;\n this.pendingToasts.forEach((toast) => {\n this.add(toast);\n });\n this.pendingToasts = null;\n }, $wire.loadDelay);\n },\n\n fetchWireToasts () {\n this.wireToasts.forEach((toast, i) => {\n if (i < this.wireToastsIndex) {\n return;\n }\n\n this.add(window.Alpine.raw(toast));\n\n this.wireToastsIndex++;\n });\n },\n\n add (toast) {\n if (this.loaded !== true) {\n this.pendingToasts.push(toast);\n\n return;\n }\n\n if (toast.type === 'debug') {\n if (this.prod) {\n return;\n }\n\n console.log(toast.title, toast.message);\n }\n\n toast.type ??= 'info';\n toast.show = 0;\n toast.index = this.count;\n\n this.toasts[this.count] = toast;\n\n this.scheduleRemoval(this.count);\n\n this.count++;\n },\n\n scheduleRemoval (toastIndex) {\n if (Object.keys(this.pendingRemovals).includes(toastIndex.toString())) {\n return;\n }\n\n if (this.toasts[toastIndex].duration === 0) {\n return;\n }\n\n this.pendingRemovals[toastIndex] = setTimeout(() => {\n this.remove(toastIndex);\n }, this.toasts[toastIndex].duration);\n },\n\n scheduleRemovalWithOlder (toastIndex = this.count) {\n for (let i = 0; i < toastIndex; i++) {\n this.scheduleRemoval(i);\n }\n },\n\n cancelRemovalWithNewer (toastIndex) {\n for (let i = this.count - 1; i >= toastIndex; i--) {\n clearTimeout(this.pendingRemovals[i]);\n delete this.pendingRemovals[i];\n }\n },\n\n remove (index) {\n if (this.toasts[index]) {\n this.toasts[index].show = 0;\n }\n\n setTimeout(() => {\n this.toasts[index] = '';\n delete this.pendingRemovals[index];\n }, 500);\n }\n\n })\n );\n}\n"],"names":["document","addEventListener","window","Alpine","data","$wire","defaultDuration","wireToasts","$entangle","prod","wireToastsIndex","toasts","pendingToasts","pendingRemovals","count","loaded","init","_this","this","Toast","component","make","message","title","type","duration","debug","arguments","length","undefined","add","info","success","warning","danger","event","detail","fetchWireToasts","$watch","setTimeout","forEach","toast","loadDelay","_this2","i","raw","_toast$type","console","log","show","index","scheduleRemoval","push","toastIndex","_this3","Object","keys","includes","toString","remove","scheduleRemovalWithOlder","cancelRemovalWithNewer","clearTimeout","_this4"],"mappings":"6GAEAA,SAASC,iBAAiB,uBAAuB,WACzCC,OAAOC,OCFNC,KAAK,kBACV,SAACC,OAAK,MAAM,CACVC,gBAAiBD,MAAMC,gBACvBC,WAAYF,MAAMG,UAAU,UAC5BC,KAAMJ,MAAMG,UAAU,QACtBE,gBAAiB,EACjBC,OAAQ,GACRC,cAAe,GACfC,gBAAiB,GACjBC,MAAO,EACPC,QAAQ,EAERC,KAAI,WAAI,IAAAC,MAAAC,KACNhB,OAAOiB,MAAQ,CACbC,UAAWF,KAEXG,KAAM,SAACC,QAASC,MAAOC,KAAMC,UAAQ,MAAM,CAAEF,MAAAA,MAAOD,QAAAA,QAASE,KAAAA,KAAMC,SAAAA,SAAW,EAE9EC,MAAK,SAAEJ,SAA2C,IAAlCC,MAAKI,UAAAC,OAAA,QAAAC,IAAAF,UAAA,GAAAA,UAAA,GAAG,GAAIF,SAAQE,UAAAC,OAAA,QAAAC,IAAAF,UAAA,GAAAA,UAAA,QAAGE,EACrCX,KAAKE,UAAUU,IAAIZ,KAAKG,KAAKC,QAASC,MAAO,QAASE,eAAAA,SAAYP,KAAKE,UAAUd,iBAClF,EAEDyB,KAAI,SAAET,SAA2C,IAAlCC,MAAKI,UAAAC,OAAA,QAAAC,IAAAF,UAAA,GAAAA,UAAA,GAAG,GAAIF,SAAQE,UAAAC,OAAA,QAAAC,IAAAF,UAAA,GAAAA,UAAA,QAAGE,EACpCX,KAAKE,UAAUU,IAAIZ,KAAKG,KAAKC,QAASC,MAAO,OAAQE,eAAAA,SAAYP,KAAKE,UAAUd,iBACjF,EAED0B,QAAO,SAAEV,SAA2C,IAAlCC,MAAKI,UAAAC,OAAA,QAAAC,IAAAF,UAAA,GAAAA,UAAA,GAAG,GAAIF,SAAQE,UAAAC,OAAA,QAAAC,IAAAF,UAAA,GAAAA,UAAA,QAAGE,EACvCX,KAAKE,UAAUU,IAAIZ,KAAKG,KAAKC,QAASC,MAAO,UAAWE,eAAAA,SAAYP,KAAKE,UAAUd,iBACpF,EAED2B,QAAO,SAAEX,SAA2C,IAAlCC,MAAKI,UAAAC,OAAA,QAAAC,IAAAF,UAAA,GAAAA,UAAA,GAAG,GAAIF,SAAQE,UAAAC,OAAA,QAAAC,IAAAF,UAAA,GAAAA,UAAA,QAAGE,EACvCX,KAAKE,UAAUU,IAAIZ,KAAKG,KAAKC,QAASC,MAAO,UAAWE,eAAAA,SAAYP,KAAKE,UAAUd,iBACpF,EAED4B,OAAM,SAAEZ,SAA2C,IAAlCC,MAAKI,UAAAC,OAAA,QAAAC,IAAAF,UAAA,GAAAA,UAAA,GAAG,GAAIF,SAAQE,UAAAC,OAAA,QAAAC,IAAAF,UAAA,GAAAA,UAAA,QAAGE,EACtCX,KAAKE,UAAUU,IAAIZ,KAAKG,KAAKC,QAASC,MAAO,SAAUE,eAAAA,SAAYP,KAAKE,UAAUd,iBACpF,GAGFL,iBAAiB,SAAS,SAACkC,OACzBlB,MAAKa,IAAIK,MAAMC,OACjB,IAEAlB,KAAKmB,kBAELnB,KAAKoB,OAAO,cAAc,WACxBrB,MAAKoB,iBACP,IAEAE,YAAW,WACTtB,MAAKF,QAAS,EACdE,MAAKL,cAAc4B,SAAQ,SAACC,OAC1BxB,MAAKa,IAAIW,MACX,IACAxB,MAAKL,cAAgB,IACvB,GAAGP,MAAMqC,UACV,EAEDL,gBAAe,WAAI,IAAAM,OAAAzB,KACjBA,KAAKX,WAAWiC,SAAQ,SAACC,MAAOG,GAC1BA,EAAID,OAAKjC,kBAIbiC,OAAKb,IAAI5B,OAAOC,OAAO0C,IAAIJ,QAE3BE,OAAKjC,kBACP,GACD,EAEDoB,IAAG,SAAEW,OAAO,IAAAK,YACV,IAAoB,IAAhB5B,KAAKH,OAAT,CAMA,GAAmB,UAAf0B,MAAMjB,KAAkB,CAC1B,GAAIN,KAAKT,KACP,OAGFsC,QAAQC,IAAIP,MAAMlB,MAAOkB,MAAMnB,QACjC,CAEUwB,QAAVA,YAAAL,MAAMjB,YAAIsB,IAAAA,cAAVL,MAAMjB,KAAS,QACfiB,MAAMQ,KAAO,EACbR,MAAMS,MAAQhC,KAAKJ,MAEnBI,KAAKP,OAAOO,KAAKJ,OAAS2B,MAE1BvB,KAAKiC,gBAAgBjC,KAAKJ,OAE1BI,KAAKJ,OAlBL,MAHEI,KAAKN,cAAcwC,KAAKX,MAsB3B,EAEDU,gBAAe,SAAEE,YAAY,IAAAC,OAAApC,KACvBqC,OAAOC,KAAKtC,KAAKL,iBAAiB4C,SAASJ,WAAWK,aAIjB,IAArCxC,KAAKP,OAAO0C,YAAY5B,WAI5BP,KAAKL,gBAAgBwC,YAAcd,YAAW,WAC5Ce,OAAKK,OAAON,WACb,GAAEnC,KAAKP,OAAO0C,YAAY5B,UAC5B,EAEDmC,yBAAwB,WACtB,IADiD,IAAzBP,WAAU1B,UAAAC,OAAAD,QAAAE,IAAAF,UAAAE,GAAAF,UAAG,GAAAT,KAAKJ,MACjC8B,EAAI,EAAGA,EAAIS,WAAYT,IAC9B1B,KAAKiC,gBAAgBP,EAExB,EAEDiB,uBAAsB,SAAER,YACtB,IAAK,IAAIT,EAAI1B,KAAKJ,MAAQ,EAAG8B,GAAKS,WAAYT,IAC5CkB,aAAa5C,KAAKL,gBAAgB+B,WAC3B1B,KAAKL,gBAAgB+B,EAE/B,EAEDe,OAAM,SAAET,OAAO,IAAAa,OAAA7C,KACTA,KAAKP,OAAOuC,SACdhC,KAAKP,OAAOuC,OAAOD,KAAO,GAG5BV,YAAW,WACTwB,OAAKpD,OAAOuC,OAAS,UACda,OAAKlD,gBAAgBqC,MAC7B,GAAE,IACL,EAED,GDnIL"} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tall-toasts", 3 | "author": "John F (#usernotnull)", 4 | "license": "MIT", 5 | "scripts": { 6 | "build": "npx rollup -c", 7 | "watch": "npx rollup -c -w" 8 | }, 9 | "devDependencies": { 10 | "@babel/core": "^7.27", 11 | "@babel/preset-env": "^7.27", 12 | "@babel/plugin-proposal-object-rest-spread": "^7.20", 13 | "@rollup/plugin-alias": "^5.1", 14 | "@rollup/plugin-node-resolve": "^15.3", 15 | "@rollup/plugin-alias": "^5.1", 16 | "@rollup/plugin-commonjs": "^25.0", 17 | "@rollup/plugin-babel": "^6.0", 18 | "core-js": "^3.42", 19 | "fs-extra": "^11.3", 20 | "get-value": "^3.0", 21 | "md5": "^2.3", 22 | "rollup": "^2.79", 23 | "rollup-plugin-filesize": "^10.0", 24 | "rollup-plugin-output-manifest": "^2.0", 25 | "rollup-plugin-terser": "^7.0", 26 | "whatwg-fetch": "^3.6" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | The Laravel Coding Standards 12 | 13 | 21 | src 22 | config 23 | tests 24 | 25 | 30 | */cache/* 31 | */*.js 32 | */*.css 33 | */*.xml 34 | */*.blade.php 35 | */autoload.php 36 | */vendor/* 37 | index.php 38 | 39 | 47 | 48 | 49 | 50 | 51 | 55 | 56 | 57 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /phpmd-ruleset.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | Inspired by https://github.com/phpmd/phpmd/issues/137 9 | using http://phpmd.org/documentation/creating-a-ruleset.html 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 47 | 3 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | includes: 2 | - ./vendor/nunomaduro/larastan/extension.neon 3 | 4 | parameters: 5 | paths: 6 | - src 7 | 8 | # The level 9 is the highest level 9 | level: 5 10 | 11 | excludePaths: 12 | - ./src/ToastServiceProvider.php 13 | 14 | ignoreErrors: 15 | - '#Unsafe usage of new static#' 16 | - '#Call to an undefined method Illuminate\\Support\\HigherOrder#' 17 | - '#Call to an undefined static method#' 18 | - '#Parameter (\#)(\d) \$callback of method Illuminate\\Container\\Container::call\(\) expects \(callable\(\): (.*)#' 19 | - '#Parameter (\#)(\d) \$callback of function view expects view-string|null, string given#' 20 | 21 | reportUnmatchedIgnoredErrors: false 22 | checkOctaneCompatibility: true 23 | -------------------------------------------------------------------------------- /resources/js/tall-toasts.js: -------------------------------------------------------------------------------- 1 | export default function (Alpine) { 2 | Alpine.data('ToastComponent', 3 | ($wire) => ({ 4 | defaultDuration: $wire.defaultDuration, 5 | wireToasts: $wire.$entangle('toasts'), 6 | prod: $wire.$entangle('prod'), 7 | wireToastsIndex: 0, 8 | toasts: [], 9 | pendingToasts: [], 10 | pendingRemovals: [], 11 | count: 0, 12 | loaded: false, 13 | 14 | init () { 15 | window.Toast = { 16 | component: this, 17 | 18 | make: (message, title, type, duration) => ({ title, message, type, duration }), 19 | 20 | debug (message, title = '', duration = undefined) { 21 | this.component.add(this.make(message, title, 'debug', duration ?? this.component.defaultDuration)); 22 | }, 23 | 24 | info (message, title = '', duration = undefined) { 25 | this.component.add(this.make(message, title, 'info', duration ?? this.component.defaultDuration)); 26 | }, 27 | 28 | success (message, title = '', duration = undefined) { 29 | this.component.add(this.make(message, title, 'success', duration ?? this.component.defaultDuration)); 30 | }, 31 | 32 | warning (message, title = '', duration = undefined) { 33 | this.component.add(this.make(message, title, 'warning', duration ?? this.component.defaultDuration)); 34 | }, 35 | 36 | danger (message, title = '', duration = undefined) { 37 | this.component.add(this.make(message, title, 'danger', duration ?? this.component.defaultDuration)); 38 | } 39 | }; 40 | 41 | addEventListener('toast', (event) => { 42 | this.add(event.detail); 43 | }); 44 | 45 | this.fetchWireToasts(); 46 | 47 | this.$watch('wireToasts', () => { 48 | this.fetchWireToasts(); 49 | }); 50 | 51 | setTimeout(() => { 52 | this.loaded = true; 53 | this.pendingToasts.forEach((toast) => { 54 | this.add(toast); 55 | }); 56 | this.pendingToasts = null; 57 | }, $wire.loadDelay); 58 | }, 59 | 60 | fetchWireToasts () { 61 | this.wireToasts.forEach((toast, i) => { 62 | if (i < this.wireToastsIndex) { 63 | return; 64 | } 65 | 66 | this.add(window.Alpine.raw(toast)); 67 | 68 | this.wireToastsIndex++; 69 | }); 70 | }, 71 | 72 | add (toast) { 73 | if (this.loaded !== true) { 74 | this.pendingToasts.push(toast); 75 | 76 | return; 77 | } 78 | 79 | if (toast.type === 'debug') { 80 | if (this.prod) { 81 | return; 82 | } 83 | 84 | console.log(toast.title, toast.message); 85 | } 86 | 87 | toast.type ??= 'info'; 88 | toast.show = 0; 89 | toast.index = this.count; 90 | 91 | this.toasts[this.count] = toast; 92 | 93 | this.scheduleRemoval(this.count); 94 | 95 | this.count++; 96 | }, 97 | 98 | scheduleRemoval (toastIndex) { 99 | if (Object.keys(this.pendingRemovals).includes(toastIndex.toString())) { 100 | return; 101 | } 102 | 103 | if (this.toasts[toastIndex].duration === 0) { 104 | return; 105 | } 106 | 107 | this.pendingRemovals[toastIndex] = setTimeout(() => { 108 | this.remove(toastIndex); 109 | }, this.toasts[toastIndex].duration); 110 | }, 111 | 112 | scheduleRemovalWithOlder (toastIndex = this.count) { 113 | for (let i = 0; i < toastIndex; i++) { 114 | this.scheduleRemoval(i); 115 | } 116 | }, 117 | 118 | cancelRemovalWithNewer (toastIndex) { 119 | for (let i = this.count - 1; i >= toastIndex; i--) { 120 | clearTimeout(this.pendingRemovals[i]); 121 | delete this.pendingRemovals[i]; 122 | } 123 | }, 124 | 125 | remove (index) { 126 | if (this.toasts[index]) { 127 | this.toasts[index].show = 0; 128 | } 129 | 130 | setTimeout(() => { 131 | this.toasts[index] = ''; 132 | delete this.pendingRemovals[index]; 133 | }, 500); 134 | } 135 | 136 | }) 137 | ); 138 | } 139 | -------------------------------------------------------------------------------- /resources/views/includes/content.blade.php: -------------------------------------------------------------------------------- 1 |
10 |
11 |
12 |
17 | 18 |
23 |
24 | 25 | @include('tall-toasts::includes.icon') 26 |
27 |
28 | -------------------------------------------------------------------------------- /resources/views/includes/icon.blade.php: -------------------------------------------------------------------------------- 1 | 14 | 15 | 32 | 33 | 45 | 46 | 63 | 64 | 81 | -------------------------------------------------------------------------------- /resources/views/livewire/toasts.blade.php: -------------------------------------------------------------------------------- 1 |
7 | 27 |
28 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import md5 from 'md5'; 2 | import fs from 'fs-extra'; 3 | import path from 'node:path'; 4 | import babel from '@rollup/plugin-babel'; 5 | import alias from '@rollup/plugin-alias'; 6 | import filesize from 'rollup-plugin-filesize'; 7 | import { terser } from 'rollup-plugin-terser'; 8 | import commonjs from '@rollup/plugin-commonjs'; 9 | import resolve from '@rollup/plugin-node-resolve'; 10 | import outputManifest from 'rollup-plugin-output-manifest'; 11 | 12 | export default { 13 | input: 'builds/cdn.js', 14 | output: { 15 | format: 'umd', 16 | sourcemap: true, 17 | name: 'tall-toasts', 18 | file: 'dist/js/tall-toasts.js' 19 | }, 20 | plugins: [ 21 | resolve(), 22 | commonjs({ 23 | include: /node_modules\/(get-value|isobject|core-js)/ 24 | }), 25 | filesize(), 26 | terser({ 27 | mangle: false, 28 | compress: { 29 | drop_debugger: false 30 | } 31 | }), 32 | babel({ 33 | exclude: 'node_modules/**' 34 | }), 35 | alias({ 36 | entries: [ 37 | { find: '@', replacement: path.resolve('resources/js') } 38 | ] 39 | }), 40 | 41 | outputManifest({ 42 | serialize () { 43 | const file = fs.readFileSync(path.resolve('dist/js/tall-toasts.js'), 'utf8'); 44 | const hash = md5(file).substr(0, 20); 45 | 46 | return JSON.stringify({ 47 | '/tall-toasts.js': '/tall-toasts.js?id=' + hash 48 | }); 49 | } 50 | }) 51 | ] 52 | }; 53 | -------------------------------------------------------------------------------- /src/Concerns/WireToast.php: -------------------------------------------------------------------------------- 1 | dispatch( 16 | 'toast', 17 | message: $notification['message'], 18 | title: $notification['title'], 19 | type: $notification['type'], 20 | duration: $notification['duration'], 21 | ); 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Controllers/CanPretendToBeAFile.php: -------------------------------------------------------------------------------- 1 | matchesCache($lastModified)) { 35 | return response()->make('', 304, [ 36 | 'Expires' => $this->httpDate($expires), 37 | 'Cache-Control' => $cacheControl, 38 | ]); 39 | } 40 | 41 | return response()->file($file, [ 42 | 'Content-Type' => "$mimeType; charset=utf-8", 43 | 'Expires' => $this->httpDate($expires), 44 | 'Cache-Control' => $cacheControl, 45 | 'Last-Modified' => $this->httpDate($lastModified), 46 | ]); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Controllers/JavaScriptAssets.php: -------------------------------------------------------------------------------- 1 | pretendResponseIsFile(__DIR__ . '/../../dist/js/tall-toasts.js.map'); 15 | } 16 | 17 | public function source(): Response|BinaryFileResponse 18 | { 19 | return $this->pretendResponseIsFile(__DIR__ . '/../../dist/js/tall-toasts.js'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Livewire/ToastComponent.php: -------------------------------------------------------------------------------- 1 | toasts = array_merge($this->toasts, ToastManager::pull()); 30 | } 31 | 32 | public function mount(): void 33 | { 34 | if (session()->has(config('tall-toasts.session_keys.toasts_next_page'))) { 35 | $this->toasts = ToastManager::pullNextPage(); 36 | } 37 | 38 | $this->loadDelay = config('tall-toasts.load_delay'); 39 | 40 | $this->prod = App::isProduction(); 41 | 42 | $this->defaultDuration = config('tall-toasts.duration'); 43 | } 44 | 45 | public function render(): View|Factory|Application 46 | { 47 | return app(ViewFactory::class)->make('tall-toasts::livewire.toasts'); 48 | } 49 | 50 | public function updatedProd(): void 51 | { 52 | $this->prod = App::isProduction(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Notification.php: -------------------------------------------------------------------------------- 1 | duration = $duration; 26 | $this->type = $type; 27 | $this->title = $title; 28 | $this->message = $message; 29 | $this->sanitize = $type !== NotificationType::$debug; 30 | } 31 | 32 | protected function asArray(): array 33 | { 34 | $message = $this->sanitize ? htmlspecialchars($this->message, ENT_QUOTES) : $this->message; 35 | $title = $this->sanitize && $this->title ? htmlspecialchars($this->title, ENT_QUOTES) : $this->title; 36 | $type = $this->type ?? NotificationType::$info; 37 | $duration = $this->duration ?? config('tall-toasts.duration'); 38 | 39 | return compact('message', 'title', 'type', 'duration'); 40 | } 41 | 42 | public function doNotSanitize(): Notification 43 | { 44 | $this->sanitize = false; 45 | 46 | return $this; 47 | } 48 | 49 | public static function make( 50 | string $message, 51 | ?string $title, 52 | ?string $type = null, 53 | ?int $duration = null 54 | ): array { 55 | return (new static($message, $title, $type, $duration))->asArray(); 56 | } 57 | 58 | public function duration(int $duration): Notification 59 | { 60 | $this->duration = $duration; 61 | 62 | return $this; 63 | } 64 | 65 | public function sticky(): Notification 66 | { 67 | $this->duration = 0; 68 | 69 | return $this; 70 | } 71 | 72 | public function push(): void 73 | { 74 | session()->push(config('tall-toasts.session_keys.toasts'), $this->asArray()); 75 | } 76 | 77 | public function pushOnNextPage(): void 78 | { 79 | session()->push(config('tall-toasts.session_keys.toasts_next_page'), $this->asArray()); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/NotificationType.php: -------------------------------------------------------------------------------- 1 | componentRendered; 21 | } 22 | 23 | protected static function filterNotifications(array $notifications): array 24 | { 25 | return collect($notifications) 26 | ->filter( 27 | fn (array $notification) => ! App::isProduction() || $notification['type'] !== NotificationType::$debug 28 | ) 29 | ->values() 30 | ->toArray(); 31 | } 32 | 33 | protected static function getFacadeAccessor(): string 34 | { 35 | return 'toast'; 36 | } 37 | 38 | public static function hasPendingToasts(): bool 39 | { 40 | return session()->has(config('tall-toasts.session_keys.toasts')); 41 | } 42 | 43 | protected static function javaScriptAssets(array $options): string 44 | { 45 | $appUrl = config('toast.asset_url') ?: rtrim($options['asset_url'] ?? '', '/'); 46 | 47 | $manifestContent = File::get(__DIR__ . '/../dist/js/manifest.json'); 48 | 49 | $manifest = json_decode($manifestContent, true, 512, JSON_THROW_ON_ERROR); 50 | $versionedFileName = $manifest['/tall-toasts.js']; 51 | 52 | // Default to dynamic `tall-toasts.js` (served by a Laravel route). 53 | $fullAssetPath = "{$appUrl}/toast{$versionedFileName}"; 54 | 55 | $nonce = isset($options['nonce']) ? "nonce=\"{$options['nonce']}\"" : ''; 56 | 57 | // Adding semicolons for this JavaScript is important, 58 | // because it will be minified in production. 59 | return << 61 | 66 | HTML; 67 | } 68 | 69 | protected static function minify(string $subject): ?string 70 | { 71 | return preg_replace('~(\v|\t|\s{2,})~m', '', $subject); 72 | } 73 | 74 | public static function pull(): array 75 | { 76 | return self::filterNotifications(session()->pull(config('tall-toasts.session_keys.toasts'), [])); 77 | } 78 | 79 | public static function pullNextPage(): array 80 | { 81 | return self::filterNotifications(session()->pull(config('tall-toasts.session_keys.toasts_next_page'), [])); 82 | } 83 | 84 | public static function scripts(array $options = []): string 85 | { 86 | $debug = config('app.debug'); 87 | 88 | $scripts = self::javaScriptAssets($options); 89 | 90 | $html = $debug ? [''] : []; 91 | 92 | $html[] = $debug ? $scripts : self::minify($scripts); 93 | 94 | return implode("\n", $html); 95 | } 96 | 97 | public static function setComponentRendered(bool $rendered): void 98 | { 99 | app('toast.manager')->componentRendered = $rendered; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/ToastServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('tall-toasts') 21 | ->hasConfigFile() 22 | ->hasViews(); 23 | } 24 | 25 | public function packageBooted(): void 26 | { 27 | RouteFacade::get('/toast/tall-toasts.js', [JavaScriptAssets::class, 'source']); 28 | RouteFacade::get('/toast/tall-toasts.js.map', [JavaScriptAssets::class, 'maps']); 29 | 30 | Blade::directive('toastScripts', [ToastBladeDirectives::class, 'toastScripts']); 31 | 32 | Livewire::component('toasts', ToastComponent::class); 33 | } 34 | 35 | public function registeringPackage(): void 36 | { 37 | $this->app->singleton(Toast::class); 38 | $this->app->alias(Toast::class, 'toast'); 39 | 40 | $this->app->singleton(ToastManager::class); 41 | $this->app->alias(ToastManager::class, 'toast.manager'); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/helpers.php: -------------------------------------------------------------------------------- 1 |