toastify 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +21 -0
- data/README.md +120 -0
- data/lib/generators/toastify/install/install_generator.rb +13 -0
- data/lib/generators/toastify/install/templates/toastify.rb +11 -0
- data/lib/toastify/application_helper.rb +93 -0
- data/lib/toastify/assets/toastify.css +166 -0
- data/lib/toastify/assets/toastify.js +185 -0
- data/lib/toastify/controller.rb +29 -0
- data/lib/toastify/engine.rb +17 -0
- data/lib/toastify/version.rb +3 -0
- data/lib/toastify.rb +7 -0
- metadata +73 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 90839e2783e1a2fb7011195f55d7fef2d41407a7e6d0559f4d00751315895b2a
|
|
4
|
+
data.tar.gz: 916288f67f30353a27a67de7168dc146f6109a0592c8218f24493655d46dd43a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 16da282d92daea5d5d67d4d70a5ef6db58cec9cf877d4c785cb7b3cf9ef21b805cce68a60da09180c726570e217a80dce45ce66039ab7c81e7d5964c8d6e7a5e
|
|
7
|
+
data.tar.gz: 3e27ec8a357376fb25bc22b64f9ac370b83f39f0bbba2522c031f03e7726fb8b37dd14f9ca70014cc4f230a73133278ed5919d3cc39c22dfaf4328b9b3ef313c
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Toastify
|
|
2
|
+
|
|
3
|
+
`toastify` packages a lightweight toast notification system for Rails applications.
|
|
4
|
+
|
|
5
|
+
Compatible with Rails 4+ and also integrates seamlessly with Turbo Stream requests.
|
|
6
|
+
It can also be used directly in JavaScript (e.g., `Toastify.success("Saved!")`).
|
|
7
|
+
|
|
8
|
+
### Example Images
|
|
9
|
+
|
|
10
|
+
<img width="346" height="85" alt="Light Theme Example" src="https://github.com/user-attachments/assets/b7a3123a-89a3-4f49-97ff-aa25f4b870d0" />
|
|
11
|
+
|
|
12
|
+
<img width="338" height="87" alt="Dark Theme Example" src="https://github.com/user-attachments/assets/65671ac6-91d7-471f-9c03-6458023f3ae2" />
|
|
13
|
+
|
|
14
|
+
<img width="339" height="83" alt="Colored Theme Example" src="https://github.com/user-attachments/assets/1c99ffdb-dc3c-4fef-aa16-b7eff08e0cde" />
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
Add this line to your application's Gemfile:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
gem "toastify"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Then run:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
bundle install
|
|
28
|
+
bin/rails generate toastify:install
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
Simply add the `<%= toastify_tag %>` tag to your layout file `app/views/layouts/application.html.erb`. You do **not** need to install any JavaScript or CSS manually.
|
|
34
|
+
|
|
35
|
+
```erb
|
|
36
|
+
<body>
|
|
37
|
+
<%= yield %>
|
|
38
|
+
<%= toastify_tag %>
|
|
39
|
+
</body>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Then, assign ordinary flash messages in your controllers to trigger toasts:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
def create
|
|
46
|
+
@post = Post.create!(post_params)
|
|
47
|
+
redirect_to posts_path, success: "Post created successfully!"
|
|
48
|
+
end
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
If you are using Turbo Streams:
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
def update
|
|
55
|
+
@post.update!(post_params)
|
|
56
|
+
flash.now[:success] = "Updated successfully!"
|
|
57
|
+
respond_to do |format|
|
|
58
|
+
format.js
|
|
59
|
+
format.turbo_stream
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Configuration
|
|
65
|
+
|
|
66
|
+
### Global Defaults
|
|
67
|
+
|
|
68
|
+
You can configure global defaults in the generated initializer file `config/initializers/toastify.rb`:
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
Rails.application.config.toastify = {
|
|
72
|
+
position: "top-right", # top-right, top-left, top-center, bottom-right, bottom-left, bottom-center
|
|
73
|
+
auto_close: 5000, # duration in milliseconds
|
|
74
|
+
theme: "light", # light, dark, colored
|
|
75
|
+
transition: "slide", # slide, bounce, zoom, flip, fade
|
|
76
|
+
close_button: true, # true, false
|
|
77
|
+
pause_on_hover: true, # true, false
|
|
78
|
+
draggable: true # true, false
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Per-Request Overrides
|
|
83
|
+
|
|
84
|
+
You can also customize the toast behaviors on a per-request basis by setting these specific `flash` keys in your controllers:
|
|
85
|
+
|
|
86
|
+
| Property | Type | Default | Description |
|
|
87
|
+
| --- | --- | --- | --- |
|
|
88
|
+
| `flash[:toast_position]` | `String` | `"top-right"` | Position of the toast (`top-right`, `top-left`, `top-center`, `bottom-right`, `bottom-left`, `bottom-center`). |
|
|
89
|
+
| `flash[:toast_duration]` | `Integer` | `5000` | Duration in milliseconds before the toast auto-closes. |
|
|
90
|
+
| `flash[:toast_theme]` | `String` | `"light"` | Visual theme of the toast (`light`, `dark`, or `colored`). |
|
|
91
|
+
| `flash[:toast_transition]` | `String` | `"slide"` | Animation type (`slide`, `zoom`, `flip`, `bounce`). |
|
|
92
|
+
| `flash[:toast_close_button]`| `Boolean` | `true` | Show or hide the close button. |
|
|
93
|
+
| `flash[:toast_pause_on_hover]`| `Boolean` | `true` | Pause auto-closing when the mouse hovers over the toast. |
|
|
94
|
+
| `flash[:toast_draggable]` | `Boolean` | `true` | Allow the toast to be dragged to close. |
|
|
95
|
+
|
|
96
|
+
## JavaScript usage
|
|
97
|
+
|
|
98
|
+
Toastify exposes a global object if you wish to trigger toasts manually from your JavaScript code:
|
|
99
|
+
|
|
100
|
+
```javascript
|
|
101
|
+
Toastify.success("Saved!")
|
|
102
|
+
Toastify.error("Failed to save", { autoClose: 8000, theme: "colored" })
|
|
103
|
+
Toastify.info("Syncing...", { position: "bottom-center", transition: "zoom" })
|
|
104
|
+
Toastify.warning("Low storage", { theme: "dark", transition: "flip" })
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### JavaScript Configuration Options
|
|
108
|
+
|
|
109
|
+
When calling the JavaScript methods (e.g., `Toastify.success()`, `Toastify.show()`), you can pass an `options` object as the second argument with the following properties:
|
|
110
|
+
|
|
111
|
+
| Property | Type | Default | Description |
|
|
112
|
+
| --- | --- | --- | --- |
|
|
113
|
+
| `position` | `String` | `"top-right"` | Position of the toast (`top-right`, `top-left`, `top-center`, `bottom-right`, `bottom-left`, `bottom-center`). |
|
|
114
|
+
| `autoClose` | `Number` | `5000` | Duration in milliseconds before the toast auto-closes. |
|
|
115
|
+
| `theme` | `String` | `"light"` | Visual theme of the toast (`light`, `dark`, or `colored`). |
|
|
116
|
+
| `transition` | `String` | `"slide"` | Animation type (`slide`, `zoom`, `flip`, `bounce`). |
|
|
117
|
+
| `closeButton` | `Boolean` | `true` | Show or hide the close button. |
|
|
118
|
+
| `pauseOnHover` | `Boolean` | `true` | Pause auto-closing when the mouse hovers over the toast. |
|
|
119
|
+
| `draggable` | `Boolean` | `true` | Allow the toast to be dragged to close. |
|
|
120
|
+
| `type` | `String` | `"default"` | Toast styling type (`success`, `error`, `warning`, `info`, `default`). Automatically applied when using helper methods like `.success()`. |
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
|
|
3
|
+
module Toastify
|
|
4
|
+
module Generators
|
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
|
6
|
+
source_root File.expand_path("templates", __dir__)
|
|
7
|
+
|
|
8
|
+
def copy_initializer
|
|
9
|
+
copy_file "toastify.rb", "config/initializers/toastify.rb"
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Rails.application.config.toastify = {
|
|
4
|
+
# position: "top-right", # top-right, top-left, top-center, bottom-right, bottom-left, bottom-center
|
|
5
|
+
# auto_close: 5000, # duration in milliseconds
|
|
6
|
+
# theme: "light", # light, dark, colored
|
|
7
|
+
# transition: "slide", # slide, bounce, zoom, flip, fade
|
|
8
|
+
# close_button: true, # true, false
|
|
9
|
+
# pause_on_hover: true, # true, false
|
|
10
|
+
# draggable: true # true, false
|
|
11
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
module Toastify
|
|
2
|
+
module ApplicationHelper
|
|
3
|
+
def self.toastify_css
|
|
4
|
+
@toastify_css ||= begin
|
|
5
|
+
path = File.expand_path("assets/toastify.css", __dir__)
|
|
6
|
+
File.read(path)
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.toastify_js
|
|
11
|
+
@toastify_js ||= begin
|
|
12
|
+
path = File.expand_path("assets/toastify.js", __dir__)
|
|
13
|
+
File.read(path)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def toastify_tag
|
|
18
|
+
html = []
|
|
19
|
+
|
|
20
|
+
# On full HTML page loads, render the infrastructure
|
|
21
|
+
unless request.format.turbo_stream?
|
|
22
|
+
css_content = Toastify::ApplicationHelper.toastify_css
|
|
23
|
+
js_content = Toastify::ApplicationHelper.toastify_js
|
|
24
|
+
|
|
25
|
+
html << "<style>#{css_content}</style>"
|
|
26
|
+
html << "<div id=\"toast-container-root\" data-turbo-permanent></div>"
|
|
27
|
+
html << "<div id=\"flash-outlet\" data-turbo-cache=\"false\"></div>"
|
|
28
|
+
html << "<script type=\"module\">"
|
|
29
|
+
html << js_content
|
|
30
|
+
html << "</script>"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# For both HTML and Turbo Streams, execute the toasts if present
|
|
34
|
+
if (scripts = toastify_script_tag).present?
|
|
35
|
+
html << scripts
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
html.join("\n").html_safe
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def toastify_script_tag
|
|
42
|
+
config = Rails.application.config.try(:toastify) || {}
|
|
43
|
+
default_position = config[:position] || "top-right"
|
|
44
|
+
default_auto_close = config[:auto_close] || 5000
|
|
45
|
+
default_theme = config[:theme] || "light"
|
|
46
|
+
default_transition = config[:transition] || "slide"
|
|
47
|
+
default_close_button = config.key?(:close_button) ? config[:close_button] : true
|
|
48
|
+
default_pause_on_hover = config.key?(:pause_on_hover) ? config[:pause_on_hover] : true
|
|
49
|
+
default_draggable = config.key?(:draggable) ? config[:draggable] : true
|
|
50
|
+
|
|
51
|
+
type_map = {
|
|
52
|
+
"notice" => "info",
|
|
53
|
+
"success" => "success",
|
|
54
|
+
"alert" => "warning",
|
|
55
|
+
"error" => "error",
|
|
56
|
+
"info" => "info",
|
|
57
|
+
"warning" => "warning",
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
script_lines = []
|
|
61
|
+
flash.each do |flash_type, message|
|
|
62
|
+
next if flash_type.to_s.start_with?("toast_")
|
|
63
|
+
|
|
64
|
+
type = type_map.fetch(flash_type.to_s, "default")
|
|
65
|
+
position = flash[:toast_position] || default_position
|
|
66
|
+
auto_close = flash[:toast_duration] || default_auto_close
|
|
67
|
+
theme = flash[:toast_theme] || default_theme
|
|
68
|
+
transition = flash[:toast_transition] || default_transition
|
|
69
|
+
close_button = flash[:toast_close_button].nil? ? default_close_button : flash[:toast_close_button]
|
|
70
|
+
pause_on_hover = flash[:toast_pause_on_hover].nil? ? default_pause_on_hover : flash[:toast_pause_on_hover]
|
|
71
|
+
draggable = flash[:toast_draggable].nil? ? default_draggable : flash[:toast_draggable]
|
|
72
|
+
|
|
73
|
+
safe_message = j(message.to_s)
|
|
74
|
+
|
|
75
|
+
script_lines << "window.Toastify && window.Toastify.show('#{safe_message}', { type: '#{j(type.to_s)}', position: '#{j(position.to_s)}', autoClose: #{auto_close.to_i}, theme: '#{j(theme.to_s)}', transition: '#{j(transition.to_s)}', closeButton: #{!!close_button}, pauseOnHover: #{!!pause_on_hover}, draggable: #{!!draggable} });"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Discard so it doesn't show up again
|
|
79
|
+
flash.keys.reject { |type| type.to_s.start_with?("toast_") }.each do |type|
|
|
80
|
+
flash.discard(type)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
return nil if script_lines.empty?
|
|
84
|
+
|
|
85
|
+
html = []
|
|
86
|
+
html << "<script type=\"module\">"
|
|
87
|
+
html << " " + script_lines.join("\n ")
|
|
88
|
+
html << "</script>"
|
|
89
|
+
|
|
90
|
+
html.join("\n").html_safe
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
.Toastify__toast-container {
|
|
2
|
+
position: fixed;
|
|
3
|
+
z-index: 9999;
|
|
4
|
+
width: 320px;
|
|
5
|
+
padding: 4px;
|
|
6
|
+
box-sizing: border-box;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.Toastify__toast-container--top-right { top: 1rem; right: 1rem; }
|
|
10
|
+
.Toastify__toast-container--top-left { top: 1rem; left: 1rem; }
|
|
11
|
+
.Toastify__toast-container--top-center { top: 1rem; left: 50%; transform: translateX(-50%); }
|
|
12
|
+
.Toastify__toast-container--bottom-right { bottom: 1rem; right: 1rem; }
|
|
13
|
+
.Toastify__toast-container--bottom-left { bottom: 1rem; left: 1rem; }
|
|
14
|
+
.Toastify__toast-container--bottom-center { bottom: 1rem; left: 50%; transform: translateX(-50%); }
|
|
15
|
+
|
|
16
|
+
.Toastify__toast {
|
|
17
|
+
position: relative;
|
|
18
|
+
min-height: 64px;
|
|
19
|
+
border-radius: 4px;
|
|
20
|
+
box-shadow: 0 1px 10px rgba(0, 0, 0, .1), 0 2px 15px rgba(0, 0, 0, .05);
|
|
21
|
+
display: flex;
|
|
22
|
+
justify-content: space-between;
|
|
23
|
+
align-items: stretch;
|
|
24
|
+
overflow: hidden;
|
|
25
|
+
margin-bottom: 8px;
|
|
26
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
27
|
+
font-size: 14px;
|
|
28
|
+
cursor: default;
|
|
29
|
+
background: #fff;
|
|
30
|
+
color: #333;
|
|
31
|
+
touch-action: none;
|
|
32
|
+
user-select: none;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.Toastify__toast--dark { background: #121212; color: #fff; }
|
|
36
|
+
|
|
37
|
+
.Toastify__toast--colored.Toastify__toast--success { background: #07bc0c; color: #fff; }
|
|
38
|
+
.Toastify__toast--colored.Toastify__toast--error { background: #e74c3c; color: #fff; }
|
|
39
|
+
.Toastify__toast--colored.Toastify__toast--warning { background: #f1c40f; color: #333; }
|
|
40
|
+
.Toastify__toast--colored.Toastify__toast--info { background: #3498db; color: #fff; }
|
|
41
|
+
.Toastify__toast--colored.Toastify__toast--default { background: #fff; color: #333; }
|
|
42
|
+
|
|
43
|
+
.Toastify__toast-body {
|
|
44
|
+
display: flex;
|
|
45
|
+
align-items: center;
|
|
46
|
+
gap: 10px;
|
|
47
|
+
padding: 12px;
|
|
48
|
+
flex: 1;
|
|
49
|
+
line-height: 1.4;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.Toastify__toast-icon {
|
|
53
|
+
flex-shrink: 0;
|
|
54
|
+
display: flex;
|
|
55
|
+
align-items: center;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.Toastify__toast--success .Toastify__toast-icon { color: #07bc0c; }
|
|
59
|
+
.Toastify__toast--error .Toastify__toast-icon { color: #e74c3c; }
|
|
60
|
+
.Toastify__toast--warning .Toastify__toast-icon { color: #f1c40f; }
|
|
61
|
+
.Toastify__toast--info .Toastify__toast-icon { color: #3498db; }
|
|
62
|
+
.Toastify__toast--default .Toastify__toast-icon { color: #555; }
|
|
63
|
+
|
|
64
|
+
.Toastify__toast--colored .Toastify__toast-icon { color: inherit; }
|
|
65
|
+
|
|
66
|
+
.Toastify__close-button {
|
|
67
|
+
background: transparent;
|
|
68
|
+
border: none;
|
|
69
|
+
cursor: pointer;
|
|
70
|
+
color: inherit;
|
|
71
|
+
opacity: 0.7;
|
|
72
|
+
font-size: 14px;
|
|
73
|
+
padding: 10px;
|
|
74
|
+
align-self: flex-start;
|
|
75
|
+
transition: opacity .15s;
|
|
76
|
+
flex-shrink: 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.Toastify__close-button:hover { opacity: 1; }
|
|
80
|
+
|
|
81
|
+
.Toastify__progress-bar {
|
|
82
|
+
position: absolute;
|
|
83
|
+
bottom: 0;
|
|
84
|
+
left: 0;
|
|
85
|
+
height: 4px;
|
|
86
|
+
width: 100%;
|
|
87
|
+
transform-origin: left;
|
|
88
|
+
animation: Toastify__trackProgress linear 1 forwards;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.Toastify__progress-bar--success { background: #07bc0c; }
|
|
92
|
+
.Toastify__progress-bar--error { background: #e74c3c; }
|
|
93
|
+
.Toastify__progress-bar--warning { background: #f1c40f; }
|
|
94
|
+
.Toastify__progress-bar--info { background: #3498db; }
|
|
95
|
+
.Toastify__progress-bar--default { background: linear-gradient(to right, #4cd964, #5ac8fa, #007aff, #34aadc, #5856d6, #ff2d55); }
|
|
96
|
+
|
|
97
|
+
.Toastify__toast--dark .Toastify__progress-bar { background: #bb86fc; }
|
|
98
|
+
.Toastify__toast--colored .Toastify__progress-bar { background: rgba(255, 255, 255, .7); }
|
|
99
|
+
|
|
100
|
+
@keyframes Toastify__trackProgress {
|
|
101
|
+
0% { transform: scaleX(1); }
|
|
102
|
+
100% { transform: scaleX(0); }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.Toastify__toast--enter {
|
|
106
|
+
animation-name: var(--enter-anim);
|
|
107
|
+
animation-duration: .5s;
|
|
108
|
+
animation-fill-mode: both;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.Toastify__toast--exit {
|
|
112
|
+
animation-name: Toastify__slideOut;
|
|
113
|
+
animation-duration: .3s;
|
|
114
|
+
animation-fill-mode: both;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@keyframes Toastify__slideInRight {
|
|
118
|
+
from { transform: translateX(110%); }
|
|
119
|
+
to { transform: translateX(0); }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@keyframes Toastify__slideInLeft {
|
|
123
|
+
from { transform: translateX(-110%); }
|
|
124
|
+
to { transform: translateX(0); }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@keyframes Toastify__slideInDown {
|
|
128
|
+
from { transform: translateY(-110%); }
|
|
129
|
+
to { transform: translateY(0); }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@keyframes Toastify__slideInUp {
|
|
133
|
+
from { transform: translateY(110%); }
|
|
134
|
+
to { transform: translateY(0); }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
@keyframes Toastify__bounceInRight {
|
|
138
|
+
from, 60%, 75%, 90%, to { animation-timing-function: cubic-bezier(.215, .61, .355, 1); }
|
|
139
|
+
0% { opacity: 0; transform: translate3d(3000px, 0, 0); }
|
|
140
|
+
60% { opacity: 1; transform: translate3d(-25px, 0, 0); }
|
|
141
|
+
75% { transform: translate3d(10px, 0, 0); }
|
|
142
|
+
90% { transform: translate3d(-5px, 0, 0); }
|
|
143
|
+
to { transform: none; }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@keyframes Toastify__zoomIn {
|
|
147
|
+
from { opacity: 0; transform: scale3d(.3, .3, .3); }
|
|
148
|
+
50% { opacity: 1; }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
@keyframes Toastify__flipIn {
|
|
152
|
+
from { transform: perspective(400px) rotate3d(1, 0, 0, 90deg); animation-timing-function: ease-in; opacity: 0; }
|
|
153
|
+
40% { transform: perspective(400px) rotate3d(1, 0, 0, -20deg); animation-timing-function: ease-in; }
|
|
154
|
+
60% { transform: perspective(400px) rotate3d(1, 0, 0, 10deg); opacity: 1; }
|
|
155
|
+
80% { transform: perspective(400px) rotate3d(1, 0, 0, -5deg); }
|
|
156
|
+
to { transform: perspective(400px); }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@keyframes Toastify__fadeIn {
|
|
160
|
+
from { opacity: 0; }
|
|
161
|
+
to { opacity: 1; }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
@keyframes Toastify__slideOut {
|
|
165
|
+
to { transform: translateX(110%); opacity: 0; }
|
|
166
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
const POSITIONS = {
|
|
2
|
+
TOP_RIGHT: "top-right",
|
|
3
|
+
TOP_LEFT: "top-left",
|
|
4
|
+
TOP_CENTER: "top-center",
|
|
5
|
+
BOTTOM_RIGHT: "bottom-right",
|
|
6
|
+
BOTTOM_LEFT: "bottom-left",
|
|
7
|
+
BOTTOM_CENTER: "bottom-center",
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const THEMES = { LIGHT: "light", DARK: "dark", COLORED: "colored" }
|
|
11
|
+
const TRANSITIONS = { SLIDE: "slide", BOUNCE: "bounce", ZOOM: "zoom", FLIP: "flip", FADE: "fade" }
|
|
12
|
+
const TYPES = { SUCCESS: "success", ERROR: "error", WARNING: "warning", INFO: "info", DEFAULT: "default" }
|
|
13
|
+
|
|
14
|
+
const ICONS = {
|
|
15
|
+
success: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="20" height="20"><circle cx="12" cy="12" r="10"/><path d="M8 12l3 3 5-5"/></svg>`,
|
|
16
|
+
error: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="20" height="20"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>`,
|
|
17
|
+
warning: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="20" height="20"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>`,
|
|
18
|
+
info: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="20" height="20"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>`,
|
|
19
|
+
default: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="20" height="20"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>`,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const ENTER_ANIMS = {
|
|
23
|
+
slide: { right: "Toastify__slideInRight", left: "Toastify__slideInLeft", center: "Toastify__slideInDown", bottom: "Toastify__slideInUp" },
|
|
24
|
+
bounce: { right: "Toastify__bounceInRight", left: "Toastify__bounceInRight", center: "Toastify__bounceInRight", bottom: "Toastify__bounceInRight" },
|
|
25
|
+
zoom: { right: "Toastify__zoomIn", left: "Toastify__zoomIn", center: "Toastify__zoomIn", bottom: "Toastify__zoomIn" },
|
|
26
|
+
flip: { right: "Toastify__flipIn", left: "Toastify__flipIn", center: "Toastify__flipIn", bottom: "Toastify__flipIn" },
|
|
27
|
+
fade: { right: "Toastify__fadeIn", left: "Toastify__fadeIn", center: "Toastify__fadeIn", bottom: "Toastify__fadeIn" },
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const defaults = {
|
|
31
|
+
position: POSITIONS.TOP_RIGHT,
|
|
32
|
+
autoClose: 5000,
|
|
33
|
+
theme: THEMES.LIGHT,
|
|
34
|
+
transition: TRANSITIONS.SLIDE,
|
|
35
|
+
closeButton: true,
|
|
36
|
+
pauseOnHover: true,
|
|
37
|
+
draggable: true,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const containers = {}
|
|
41
|
+
|
|
42
|
+
function getContainer(position) {
|
|
43
|
+
if (containers[position]) return containers[position];
|
|
44
|
+
|
|
45
|
+
const el = document.createElement("div");
|
|
46
|
+
el.className = `Toastify__toast-container Toastify__toast-container--${position}`;
|
|
47
|
+
el.setAttribute("data-position", position);
|
|
48
|
+
|
|
49
|
+
// Use the permanent root if available, otherwise fall back to body
|
|
50
|
+
const root = document.getElementById("toast-container-root") || document.body;
|
|
51
|
+
root.appendChild(el);
|
|
52
|
+
|
|
53
|
+
containers[position] = el;
|
|
54
|
+
return el;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getEnterAnim(transition, position) {
|
|
58
|
+
const map = ENTER_ANIMS[transition] || ENTER_ANIMS.slide
|
|
59
|
+
if (position.includes("right")) return map.right
|
|
60
|
+
if (position.includes("left")) return map.left
|
|
61
|
+
if (position.startsWith("bottom")) return map.bottom
|
|
62
|
+
return map.center
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function dismiss(toast) {
|
|
66
|
+
if (!toast || toast._dismissing) return
|
|
67
|
+
toast._dismissing = true
|
|
68
|
+
clearTimeout(toast._timer)
|
|
69
|
+
|
|
70
|
+
toast.classList.add("Toastify__toast--exit")
|
|
71
|
+
toast.addEventListener("animationend", () => {
|
|
72
|
+
toast.remove()
|
|
73
|
+
}, { once: true })
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function show(message, options = {}) {
|
|
77
|
+
const opts = { ...defaults, ...options }
|
|
78
|
+
const { position, autoClose, theme, transition, closeButton, pauseOnHover, draggable, type = TYPES.DEFAULT } = opts
|
|
79
|
+
|
|
80
|
+
const container = getContainer(position)
|
|
81
|
+
const isBottom = position.startsWith("bottom")
|
|
82
|
+
|
|
83
|
+
const toast = document.createElement("div")
|
|
84
|
+
toast.className = [
|
|
85
|
+
"Toastify__toast",
|
|
86
|
+
`Toastify__toast--${type}`,
|
|
87
|
+
theme === THEMES.DARK ? "Toastify__toast--dark" : "",
|
|
88
|
+
theme === THEMES.COLORED ? "Toastify__toast--colored" : "",
|
|
89
|
+
].filter(Boolean).join(" ")
|
|
90
|
+
|
|
91
|
+
const enterAnim = getEnterAnim(transition, position)
|
|
92
|
+
toast.style.setProperty("--enter-anim", enterAnim)
|
|
93
|
+
toast.classList.add("Toastify__toast--enter")
|
|
94
|
+
|
|
95
|
+
const progressBar = autoClose
|
|
96
|
+
? `<div class="Toastify__progress-bar Toastify__progress-bar--${type}" style="animation-duration:${autoClose}ms;"></div>`
|
|
97
|
+
: ""
|
|
98
|
+
|
|
99
|
+
const closeBtn = closeButton
|
|
100
|
+
? `<button class="Toastify__close-button" aria-label="Close toast">✕</button>`
|
|
101
|
+
: ""
|
|
102
|
+
|
|
103
|
+
toast.innerHTML = `
|
|
104
|
+
<div class="Toastify__toast-body">
|
|
105
|
+
<div class="Toastify__toast-icon">${ICONS[type] || ICONS.default}</div>
|
|
106
|
+
<div class="Toastify__toast-message">${message}</div>
|
|
107
|
+
</div>
|
|
108
|
+
${closeBtn}
|
|
109
|
+
${progressBar}
|
|
110
|
+
`
|
|
111
|
+
|
|
112
|
+
toast.querySelector(".Toastify__close-button")?.addEventListener("click", () => dismiss(toast))
|
|
113
|
+
|
|
114
|
+
if (autoClose) {
|
|
115
|
+
toast._timer = setTimeout(() => dismiss(toast), autoClose)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (pauseOnHover && autoClose) {
|
|
119
|
+
const pb = toast.querySelector(".Toastify__progress-bar")
|
|
120
|
+
toast.addEventListener("mouseenter", () => {
|
|
121
|
+
clearTimeout(toast._timer)
|
|
122
|
+
pb?.style.setProperty("animation-play-state", "paused")
|
|
123
|
+
})
|
|
124
|
+
toast.addEventListener("mouseleave", () => {
|
|
125
|
+
pb?.style.setProperty("animation-play-state", "running")
|
|
126
|
+
toast._timer = setTimeout(() => dismiss(toast), autoClose / 2)
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (draggable) {
|
|
131
|
+
let startX = 0
|
|
132
|
+
let currentX = 0
|
|
133
|
+
let dragging = false
|
|
134
|
+
|
|
135
|
+
toast.addEventListener("pointerdown", (e) => {
|
|
136
|
+
if (e.target.closest(".Toastify__close-button")) return;
|
|
137
|
+
startX = e.clientX
|
|
138
|
+
dragging = true
|
|
139
|
+
toast.style.transition = "none"
|
|
140
|
+
toast.setPointerCapture(e.pointerId)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
toast.addEventListener("pointermove", (e) => {
|
|
144
|
+
if (!dragging) return
|
|
145
|
+
currentX = e.clientX - startX
|
|
146
|
+
toast.style.transform = `translateX(${currentX}px)`
|
|
147
|
+
toast.style.opacity = `${1 - Math.abs(currentX) / 150}`
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
toast.addEventListener("pointerup", () => {
|
|
151
|
+
if (!dragging) return
|
|
152
|
+
dragging = false
|
|
153
|
+
toast.style.transition = ""
|
|
154
|
+
if (Math.abs(currentX) > 80) {
|
|
155
|
+
dismiss(toast)
|
|
156
|
+
} else {
|
|
157
|
+
toast.style.transform = ""
|
|
158
|
+
toast.style.opacity = ""
|
|
159
|
+
}
|
|
160
|
+
currentX = 0
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (isBottom) container.appendChild(toast)
|
|
165
|
+
else container.insertBefore(toast, container.firstChild)
|
|
166
|
+
|
|
167
|
+
return toast
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const Toastify = {
|
|
171
|
+
success: (msg, opts) => show(msg, { ...opts, type: TYPES.SUCCESS }),
|
|
172
|
+
error: (msg, opts) => show(msg, { ...opts, type: TYPES.ERROR }),
|
|
173
|
+
warning: (msg, opts) => show(msg, { ...opts, type: TYPES.WARNING }),
|
|
174
|
+
info: (msg, opts) => show(msg, { ...opts, type: TYPES.INFO }),
|
|
175
|
+
show: (msg, opts) => show(msg, opts),
|
|
176
|
+
dismiss,
|
|
177
|
+
POSITIONS,
|
|
178
|
+
THEMES,
|
|
179
|
+
TRANSITIONS,
|
|
180
|
+
defaults,
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
window.Toastify = Toastify
|
|
184
|
+
|
|
185
|
+
export default Toastify
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Toastify
|
|
2
|
+
module Controller
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
included do
|
|
6
|
+
after_action :append_toastify_to_stream
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def append_toastify_to_stream
|
|
12
|
+
return unless request.format.turbo_stream?
|
|
13
|
+
|
|
14
|
+
toast_flashes = flash.reject { |type, _| type.to_s.start_with?("toast_") }
|
|
15
|
+
return unless toast_flashes.any?
|
|
16
|
+
|
|
17
|
+
script_content = helpers.toastify_script_tag
|
|
18
|
+
return if script_content.blank?
|
|
19
|
+
|
|
20
|
+
stream_tag = turbo_stream.append("flash-outlet", script_content)
|
|
21
|
+
|
|
22
|
+
if response.body.is_a?(String)
|
|
23
|
+
response.body += stream_tag.to_s
|
|
24
|
+
else
|
|
25
|
+
response.body = response.body.to_s + stream_tag.to_s
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Toastify
|
|
2
|
+
class Engine < ::Rails::Engine
|
|
3
|
+
isolate_namespace Toastify
|
|
4
|
+
|
|
5
|
+
initializer "toastify.helper" do
|
|
6
|
+
ActiveSupport.on_load(:action_view) do
|
|
7
|
+
include Toastify::ApplicationHelper
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
initializer "toastify.controller" do
|
|
12
|
+
ActiveSupport.on_load(:action_controller) do
|
|
13
|
+
include Toastify::Controller
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
data/lib/toastify.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: toastify
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- vasanthakumar-a
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-04-06 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rails
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '3.2'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '3.2'
|
|
27
|
+
description: A lightweight Rails gem providing a toast notification system. Compatible
|
|
28
|
+
with Rails 4, 5, 6, 7+ and also integrates seamlessly with Turbo Stream requests.
|
|
29
|
+
email:
|
|
30
|
+
- vasanthakumara117@gmail.com
|
|
31
|
+
executables: []
|
|
32
|
+
extensions: []
|
|
33
|
+
extra_rdoc_files: []
|
|
34
|
+
files:
|
|
35
|
+
- MIT-LICENSE
|
|
36
|
+
- README.md
|
|
37
|
+
- lib/generators/toastify/install/install_generator.rb
|
|
38
|
+
- lib/generators/toastify/install/templates/toastify.rb
|
|
39
|
+
- lib/toastify.rb
|
|
40
|
+
- lib/toastify/application_helper.rb
|
|
41
|
+
- lib/toastify/assets/toastify.css
|
|
42
|
+
- lib/toastify/assets/toastify.js
|
|
43
|
+
- lib/toastify/controller.rb
|
|
44
|
+
- lib/toastify/engine.rb
|
|
45
|
+
- lib/toastify/version.rb
|
|
46
|
+
homepage: https://github.com/vasanthakumar-a/toastify
|
|
47
|
+
licenses:
|
|
48
|
+
- MIT
|
|
49
|
+
metadata:
|
|
50
|
+
source_code_uri: https://github.com/vasanthakumar-a/toastify
|
|
51
|
+
changelog_uri: https://github.com/vasanthakumar-a/toastify/blob/main/CHANGELOG.md
|
|
52
|
+
bug_tracker_uri: https://github.com/vasanthakumar-a/toastify/issues
|
|
53
|
+
documentation_uri: https://github.com/vasanthakumar-a/toastify#readme
|
|
54
|
+
post_install_message:
|
|
55
|
+
rdoc_options: []
|
|
56
|
+
require_paths:
|
|
57
|
+
- lib
|
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
59
|
+
requirements:
|
|
60
|
+
- - ">="
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: 1.9.3
|
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
68
|
+
requirements: []
|
|
69
|
+
rubygems_version: 3.4.19
|
|
70
|
+
signing_key:
|
|
71
|
+
specification_version: 4
|
|
72
|
+
summary: Toast notifications for Rails.
|
|
73
|
+
test_files: []
|