ultimate_turbo_modal 3.2.1 → 3.3.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 +4 -4
- data/CHANGELOG.md +8 -0
- data/Gemfile.lock +1 -1
- data/README.md +85 -0
- data/VERSION +1 -1
- data/app/assets/stylesheets/ultimate_turbo_modal.css +423 -0
- data/lib/generators/ultimate_turbo_modal/base.rb +44 -0
- data/lib/generators/ultimate_turbo_modal/install_generator.rb +5 -0
- data/lib/generators/ultimate_turbo_modal/templates/ultimate_turbo_modal.rb +3 -0
- data/lib/generators/ultimate_turbo_modal/update_generator.rb +2 -0
- data/lib/ultimate_turbo_modal/base.rb +11 -8
- data/lib/ultimate_turbo_modal/configuration.rb +13 -5
- data/lib/ultimate_turbo_modal/railtie.rb +23 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b52125720fe85438df47417fd9fc02a2d9509d3da42d1abbbe392892f5c0cc8a
|
|
4
|
+
data.tar.gz: 67bad39477deb507e85a4fd2b481f4b7947c8eda4391c10c9d392d0935c50b85
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '08478369a96054c23ad7c627da033febb05c0bbd40afe48e6ed54ed3d0098fb7efc175f152a4e72b21108cce21607d047d5f7956afbe8c64acdafd3cfadd50e7'
|
|
7
|
+
data.tar.gz: 75f5307c6f8bf3fca81357bd0da3a5f9b5dca97aa977cd267bf91b9f5156ebe39643965b215e33ac1fd8671225385abf9674f90fa23309bc83208ea1d7ff1076
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [3.3.0] - 2026-09-05
|
|
4
|
+
|
|
5
|
+
- Added `close_on_submit`, which keeps a modal or drawer open after a successful form submission. Individual forms can override it with `data-modal-close-on-submit`.
|
|
6
|
+
- Drawers now support the `advance` option to push their URL to browser history, matching modals. Defaults to `false`.
|
|
7
|
+
- Fixed modals disappearing during in-frame updates that morph an already-open dialog.
|
|
8
|
+
- Fixed stale close cleanup interfering with a newer modal opened in the same frame.
|
|
9
|
+
- Fixed the vanilla flavor loading no styles on apps without a JavaScript bundler. The stylesheet now ships with the gem and can be included with `stylesheet_link_tag "ultimate_turbo_modal"`.
|
|
10
|
+
|
|
3
11
|
## [3.2.1] - 2026-05-07
|
|
4
12
|
|
|
5
13
|
- Fixed drawers closing abruptly when pressing Escape after dismissing a modal opened from inside the drawer.
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -26,6 +26,30 @@ $ bundle add ultimate_turbo_modal
|
|
|
26
26
|
$ bundle exec rails g ultimate_turbo_modal:install
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
+
### Stylesheet
|
|
30
|
+
|
|
31
|
+
The `tailwind` and `custom` flavors are styled entirely by the classes in their flavor
|
|
32
|
+
file, so they need no stylesheet.
|
|
33
|
+
|
|
34
|
+
The `vanilla` flavor ships its styles as a CSS file. The install generator adds it to
|
|
35
|
+
`app/views/layouts/application.html.erb` for you; if you need to add it by hand, put this
|
|
36
|
+
in your layout's `<head>`:
|
|
37
|
+
|
|
38
|
+
```erb
|
|
39
|
+
<%= stylesheet_link_tag "ultimate_turbo_modal", "data-turbo-track": "reload" %>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The gem puts the stylesheet on the asset pipeline's load path, so this works with
|
|
43
|
+
Propshaft and Sprockets whether you use importmaps or a JavaScript bundler. Keep it
|
|
44
|
+
*before* your own stylesheets: where the two collide at equal specificity the later one
|
|
45
|
+
wins, so loading the defaults first is what lets your own styles override them.
|
|
46
|
+
|
|
47
|
+
If you would rather pull the CSS through your bundler, the npm package also ships it:
|
|
48
|
+
|
|
49
|
+
```css
|
|
50
|
+
@import 'ultimate_turbo_modal/dist/vanilla.css';
|
|
51
|
+
```
|
|
52
|
+
|
|
29
53
|
|
|
30
54
|
## Usage
|
|
31
55
|
|
|
@@ -102,6 +126,7 @@ UltimateTurboModal.configure do |config|
|
|
|
102
126
|
config.modal do |m|
|
|
103
127
|
m.advance = false
|
|
104
128
|
m.close_button = true
|
|
129
|
+
m.close_on_submit = true
|
|
105
130
|
m.header = true
|
|
106
131
|
m.header_divider = true
|
|
107
132
|
m.footer_divider = true
|
|
@@ -111,7 +136,9 @@ UltimateTurboModal.configure do |config|
|
|
|
111
136
|
|
|
112
137
|
config.drawer do |d|
|
|
113
138
|
d.position = :right
|
|
139
|
+
d.advance = false
|
|
114
140
|
d.close_button = true
|
|
141
|
+
d.close_on_submit = true
|
|
115
142
|
d.header = true
|
|
116
143
|
d.header_divider = false
|
|
117
144
|
d.footer_divider = true
|
|
@@ -130,6 +157,7 @@ Per-instance options passed to `modal()` or `drawer()` override the defaults.
|
|
|
130
157
|
|------|---------|-------------|
|
|
131
158
|
| `advance` | `false` | When opening the modal, the URL in the URL bar will change to the URL of the view being shown in the modal. The Back button dismisses the modal and navigates back. If a URL is specified as a string (e.g. `advance: "/other-path"`), the browser history will advance, and the URL shown in the URL bar will be replaced with the value specified. |
|
|
132
159
|
| `close_button` | `true` | Shows or hide a close button (X) at the top right of the modal. |
|
|
160
|
+
| `close_on_submit` | `true` | Whether a successful form submission dismisses the modal. See [Closing on form submission](#closing-on-form-submission). |
|
|
133
161
|
| `header` | `true` | Whether to display a modal header. |
|
|
134
162
|
| `header_divider` | `true` | Whether to display a divider below the header. |
|
|
135
163
|
| `footer_divider` | `true` | Whether to display a divider above the footer. |
|
|
@@ -177,8 +205,10 @@ Link to it the same way as a modal:
|
|
|
177
205
|
|------|---------|-------------|
|
|
178
206
|
| `position` | `:right` | Which edge the drawer slides from. `:right` or `:left`. |
|
|
179
207
|
| `size` | `:md` | Width of the drawer. One of `:xs`, `:sm`, `:md`, `:lg`, `:xl`, `:"2xl"`, `:full`, or a CSS string (e.g. `"500px"`). |
|
|
208
|
+
| `advance` | `false` | When opening the drawer, the URL in the URL bar will change to the URL of the view being shown in the drawer. The Back button dismisses the drawer and navigates back. If a URL is specified as a string (e.g. `advance: "/other-path"`), the browser history will advance, and the URL shown in the URL bar will be replaced with the value specified. |
|
|
180
209
|
| `overlay` | `true` | Whether to show a backdrop overlay behind the drawer. |
|
|
181
210
|
| `close_button` | `true` | Shows or hide a close button (X). |
|
|
211
|
+
| `close_on_submit` | `true` | Whether a successful form submission dismisses the drawer. See [Closing on form submission](#closing-on-form-submission). |
|
|
182
212
|
| `header` | `true` | Whether to display a header. |
|
|
183
213
|
| `header_divider` | `false` | Whether to display a divider below the header. |
|
|
184
214
|
| `footer_divider` | `true` | Whether to display a divider above the footer. |
|
|
@@ -203,6 +233,61 @@ Link to it the same way as a modal:
|
|
|
203
233
|
| CSS string | Custom value, e.g. `"500px"` or `"50vw"` |
|
|
204
234
|
|
|
205
235
|
|
|
236
|
+
## Closing on form submission
|
|
237
|
+
|
|
238
|
+
By default, a form submitted inside a modal or drawer dismisses it once the
|
|
239
|
+
submission succeeds. Set `close_on_submit: false` to keep it open instead —
|
|
240
|
+
useful for chat composers, image uploaders, inline "add another" forms, and
|
|
241
|
+
anything else where the user is expected to submit repeatedly:
|
|
242
|
+
|
|
243
|
+
```erb
|
|
244
|
+
<%= drawer(title: "Messages", close_on_submit: false) do %>
|
|
245
|
+
<%= render "messages/list" %>
|
|
246
|
+
<%= form_with model: Message.new do |f| %>
|
|
247
|
+
<%= f.text_field :body %>
|
|
248
|
+
<%= f.submit "Send" %>
|
|
249
|
+
<% end %>
|
|
250
|
+
<% end %>
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Respond with a Turbo Stream to update the contents in place, and send
|
|
254
|
+
`turbo_stream.modal(:close)` from the server on the submissions that *should*
|
|
255
|
+
dismiss it.
|
|
256
|
+
|
|
257
|
+
Two things are worth calling out:
|
|
258
|
+
|
|
259
|
+
- **Failed submissions never dismiss.** A 422 rendering validation errors leaves
|
|
260
|
+
the modal open regardless of this setting, so errors are shown in place.
|
|
261
|
+
- **Redirects still dismiss.** If the server redirects to a page that doesn't
|
|
262
|
+
contain the modal frame, the browser is navigating away and the modal closes
|
|
263
|
+
(smoothly) even with `close_on_submit: false`.
|
|
264
|
+
|
|
265
|
+
### Per-form overrides
|
|
266
|
+
|
|
267
|
+
`data-modal-close-on-submit` on a form overrides the setting for that form
|
|
268
|
+
alone, so a single modal can mix both behaviors:
|
|
269
|
+
|
|
270
|
+
```erb
|
|
271
|
+
<%= drawer(title: "Messages", close_on_submit: false) do %>
|
|
272
|
+
<%# Stays open — inherits close_on_submit: false %>
|
|
273
|
+
<%= form_with model: Message.new do |f| %>
|
|
274
|
+
<%= f.text_field :body %>
|
|
275
|
+
<%= f.submit "Send" %>
|
|
276
|
+
<% end %>
|
|
277
|
+
|
|
278
|
+
<%# Dismisses the drawer, despite close_on_submit: false %>
|
|
279
|
+
<%= form_with model: @conversation, method: :delete,
|
|
280
|
+
data: { modal_close_on_submit: true } do |f| %>
|
|
281
|
+
<%= f.submit "Delete conversation" %>
|
|
282
|
+
<% end %>
|
|
283
|
+
<% end %>
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
It works in both directions: `data: { modal_close_on_submit: false }` on a form
|
|
287
|
+
inside a default modal keeps that one form from dismissing it. Placing the
|
|
288
|
+
attribute on a wrapping element applies it to every form inside; the nearest
|
|
289
|
+
one wins.
|
|
290
|
+
|
|
206
291
|
## Opening a Modal from a Drawer
|
|
207
292
|
|
|
208
293
|
You don't need to do anything special. Use `data-turbo-frame="modal"` like you would anywhere else, and UTMR handles the rest:
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.3.0
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
/* ========================================
|
|
2
|
+
All styles scoped to dialog.utmr to
|
|
3
|
+
prevent conflicts with end-user CSS.
|
|
4
|
+
======================================== */
|
|
5
|
+
|
|
6
|
+
dialog.utmr {
|
|
7
|
+
.hidden {
|
|
8
|
+
display: none;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.sr-only {
|
|
12
|
+
position: absolute;
|
|
13
|
+
width: 1px;
|
|
14
|
+
height: 1px;
|
|
15
|
+
padding: 0;
|
|
16
|
+
margin: -1px;
|
|
17
|
+
overflow: hidden;
|
|
18
|
+
clip: rect(0, 0, 0, 0);
|
|
19
|
+
white-space: nowrap;
|
|
20
|
+
border-width: 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* ========================================
|
|
24
|
+
Dialog reset (shared modal + drawer)
|
|
25
|
+
======================================== */
|
|
26
|
+
|
|
27
|
+
&.modal-container,
|
|
28
|
+
&.drawer-container {
|
|
29
|
+
position: fixed;
|
|
30
|
+
inset: 0;
|
|
31
|
+
padding: 0;
|
|
32
|
+
margin: 0;
|
|
33
|
+
border: none;
|
|
34
|
+
background: transparent;
|
|
35
|
+
max-width: 100vw;
|
|
36
|
+
max-height: 100dvh;
|
|
37
|
+
width: 100%;
|
|
38
|
+
height: 100%;
|
|
39
|
+
overflow-y: auto;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* ========================================
|
|
43
|
+
Modal styles
|
|
44
|
+
======================================== */
|
|
45
|
+
|
|
46
|
+
/* Backdrop transitions */
|
|
47
|
+
&.modal-container::backdrop {
|
|
48
|
+
opacity: 0;
|
|
49
|
+
transition: opacity 300ms ease-out;
|
|
50
|
+
}
|
|
51
|
+
&.modal-container[data-overlay="true"]::backdrop {
|
|
52
|
+
background-color: rgba(17, 24, 39, 0.7);
|
|
53
|
+
}
|
|
54
|
+
&.modal-container[data-overlay="false"]::backdrop {
|
|
55
|
+
background-color: transparent;
|
|
56
|
+
}
|
|
57
|
+
&.modal-container[data-overlay="true"][data-entered]::backdrop {
|
|
58
|
+
opacity: 1;
|
|
59
|
+
}
|
|
60
|
+
&.modal-container[data-closing]::backdrop {
|
|
61
|
+
transition-duration: 200ms;
|
|
62
|
+
transition-timing-function: ease-in;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* Modal enter/leave transitions */
|
|
66
|
+
&.modal-container:not([data-enter-ready]):not([data-entered]) .modal-inner {
|
|
67
|
+
visibility: hidden;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.modal-inner {
|
|
71
|
+
display: flex;
|
|
72
|
+
padding: 0.25rem;
|
|
73
|
+
padding-top: 10vh;
|
|
74
|
+
justify-content: center;
|
|
75
|
+
align-items: flex-start;
|
|
76
|
+
min-height: 100%;
|
|
77
|
+
opacity: 0;
|
|
78
|
+
translate: 0 1rem;
|
|
79
|
+
transition: opacity 300ms ease-out, translate 300ms ease-out, scale 300ms ease-out;
|
|
80
|
+
|
|
81
|
+
@media (min-width: 640px) {
|
|
82
|
+
padding: 1rem;
|
|
83
|
+
padding-bottom: 10vh;
|
|
84
|
+
align-items: center;
|
|
85
|
+
translate: none;
|
|
86
|
+
scale: 0.95;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
&.modal-container[data-entered] .modal-inner {
|
|
91
|
+
opacity: 1;
|
|
92
|
+
translate: none;
|
|
93
|
+
scale: 1;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
&.modal-container[data-closing] .modal-inner {
|
|
97
|
+
transition-duration: 200ms;
|
|
98
|
+
transition-timing-function: ease-in;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* Modal conditional styling via data attributes */
|
|
102
|
+
&.modal-container {
|
|
103
|
+
&[data-header="false"] .modal-header {
|
|
104
|
+
position: absolute;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
&[data-header-divider="true"] .modal-header {
|
|
108
|
+
border-bottom-width: 1px;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
&[data-footer-divider="true"] .modal-footer {
|
|
112
|
+
border-top-width: 1px;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
&[data-padding="true"] .modal-main {
|
|
116
|
+
padding: 1rem;
|
|
117
|
+
padding-top: 0.5rem;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.modal-main {
|
|
121
|
+
overflow-y: auto;
|
|
122
|
+
max-height: 75vh;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
&[data-title="false"] .modal-title-h {
|
|
126
|
+
display: none;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
&[data-close-button="false"] .modal-close {
|
|
130
|
+
display: none;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.modal-content {
|
|
135
|
+
overflow: hidden;
|
|
136
|
+
position: relative;
|
|
137
|
+
background-color: #ffffff;
|
|
138
|
+
transition-property: all;
|
|
139
|
+
text-align: left;
|
|
140
|
+
border-radius: 0.5rem;
|
|
141
|
+
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
|
142
|
+
max-height: 100vh;
|
|
143
|
+
|
|
144
|
+
@media (min-width: 640px) {
|
|
145
|
+
max-width: 48rem;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.modal-header {
|
|
150
|
+
display: flex;
|
|
151
|
+
padding-top: 1rem;
|
|
152
|
+
padding-bottom: 1rem;
|
|
153
|
+
justify-content: space-between;
|
|
154
|
+
align-items: center;
|
|
155
|
+
width: 100%;
|
|
156
|
+
border-top-left-radius: 0.25rem;
|
|
157
|
+
border-top-right-radius: 0.25rem;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.modal-title {
|
|
161
|
+
line-height: 1.75rem;
|
|
162
|
+
font-weight: 600;
|
|
163
|
+
padding-left: 1rem;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.modal-footer {
|
|
167
|
+
display: flex;
|
|
168
|
+
padding: 1rem;
|
|
169
|
+
border-bottom-right-radius: 0.25rem;
|
|
170
|
+
border-bottom-left-radius: 0.25rem;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.modal-close {
|
|
174
|
+
margin-right: 1rem;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.modal-close-button {
|
|
178
|
+
display: inline-flex;
|
|
179
|
+
padding: 0.375rem;
|
|
180
|
+
margin-left: auto;
|
|
181
|
+
background-color: transparent;
|
|
182
|
+
font-size: 0.875rem;
|
|
183
|
+
line-height: 1.25rem;
|
|
184
|
+
align-items: center;
|
|
185
|
+
border-radius: 0.5rem;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.modal-close-icon {
|
|
189
|
+
width: 1.25rem;
|
|
190
|
+
height: 1.25rem;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/* ========================================
|
|
194
|
+
Drawer styles
|
|
195
|
+
======================================== */
|
|
196
|
+
|
|
197
|
+
/* Drawer backdrop transitions */
|
|
198
|
+
&.drawer-container::backdrop {
|
|
199
|
+
background: transparent;
|
|
200
|
+
opacity: 0;
|
|
201
|
+
transition: opacity 300ms ease-out;
|
|
202
|
+
}
|
|
203
|
+
&.drawer-container[data-overlay="true"]::backdrop {
|
|
204
|
+
background-color: rgba(17, 24, 39, 0.7);
|
|
205
|
+
}
|
|
206
|
+
&.drawer-container[data-overlay="true"][data-entered]::backdrop {
|
|
207
|
+
opacity: 1;
|
|
208
|
+
}
|
|
209
|
+
&.drawer-container[data-closing]::backdrop {
|
|
210
|
+
transition-duration: 200ms;
|
|
211
|
+
transition-timing-function: ease-in;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/* Drawer sizing via data attributes */
|
|
215
|
+
&.drawer-container {
|
|
216
|
+
--utmr-gutter: 2.5rem;
|
|
217
|
+
|
|
218
|
+
@media (min-width: 640px) {
|
|
219
|
+
--utmr-gutter: 4rem;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
&.drawer-container[data-drawer-size="xs"] { --utmr-w: 16rem; }
|
|
223
|
+
&.drawer-container[data-drawer-size="sm"] { --utmr-w: 20rem; }
|
|
224
|
+
&.drawer-container[data-drawer-size="md"],
|
|
225
|
+
&.drawer-container[data-drawer-size=""] { --utmr-w: 24rem; }
|
|
226
|
+
&.drawer-container[data-drawer-size="lg"] { --utmr-w: 28rem; }
|
|
227
|
+
&.drawer-container[data-drawer-size="xl"] { --utmr-w: 42rem; }
|
|
228
|
+
&.drawer-container[data-drawer-size="2xl"] { --utmr-w: 56rem; }
|
|
229
|
+
&.drawer-container[data-drawer-size="full"] { --utmr-w: 100vw; }
|
|
230
|
+
|
|
231
|
+
/* Drawer direction */
|
|
232
|
+
&.drawer-container[data-drawer="left"] { --utmr-hide: -100% 0; }
|
|
233
|
+
&.drawer-container[data-drawer="left"] .drawer-panel { left: 0; }
|
|
234
|
+
&.drawer-container[data-drawer="right"] { --utmr-hide: 100% 0; }
|
|
235
|
+
&.drawer-container[data-drawer="right"] .drawer-panel { right: 0; }
|
|
236
|
+
|
|
237
|
+
/* Drawer panel transitions */
|
|
238
|
+
.drawer-panel {
|
|
239
|
+
position: absolute;
|
|
240
|
+
top: 0;
|
|
241
|
+
bottom: 0;
|
|
242
|
+
height: 100%;
|
|
243
|
+
width: min(var(--utmr-w), calc(100vw - var(--utmr-gutter)));
|
|
244
|
+
translate: var(--utmr-hide);
|
|
245
|
+
transition: translate 250ms ease-in-out;
|
|
246
|
+
will-change: translate;
|
|
247
|
+
|
|
248
|
+
@media (min-width: 640px) {
|
|
249
|
+
transition-duration: 400ms;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
&.drawer-container:not([data-enter-ready]):not([data-entered]) .drawer-panel {
|
|
253
|
+
visibility: hidden;
|
|
254
|
+
}
|
|
255
|
+
&.drawer-container[data-entered] .drawer-panel {
|
|
256
|
+
translate: 0;
|
|
257
|
+
}
|
|
258
|
+
&.drawer-container[data-closing] .drawer-panel {
|
|
259
|
+
translate: var(--utmr-hide);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/* Drawer conditional styling via data attributes */
|
|
263
|
+
&.drawer-container {
|
|
264
|
+
&[data-header="false"] .drawer-header {
|
|
265
|
+
display: none;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
&[data-header-divider="true"] .drawer-header {
|
|
269
|
+
padding-bottom: 1rem;
|
|
270
|
+
border-bottom-width: 1px;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
&[data-footer-divider="true"] .drawer-footer {
|
|
274
|
+
border-top-width: 1px;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
&[data-padding="true"] .drawer-main {
|
|
278
|
+
padding: 1rem;
|
|
279
|
+
padding-top: 0.5rem;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
&[data-title="false"] .drawer-title-h {
|
|
283
|
+
display: none;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
&[data-close-button="false"] .drawer-close {
|
|
287
|
+
display: none;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.drawer-wrapper {
|
|
292
|
+
position: absolute;
|
|
293
|
+
inset: 0;
|
|
294
|
+
overflow: hidden;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.drawer-content {
|
|
298
|
+
position: relative;
|
|
299
|
+
display: flex;
|
|
300
|
+
flex-direction: column;
|
|
301
|
+
height: 100%;
|
|
302
|
+
width: 100%;
|
|
303
|
+
background-color: #ffffff;
|
|
304
|
+
box-shadow:
|
|
305
|
+
0 20px 25px -5px rgba(0, 0, 0, 0.1),
|
|
306
|
+
0 8px 10px -6px rgba(0, 0, 0, 0.1);
|
|
307
|
+
}
|
|
308
|
+
&.drawer-container[data-padding="true"] .drawer-content {
|
|
309
|
+
padding-top: 1.5rem;
|
|
310
|
+
padding-bottom: 1.5rem;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.drawer-header {
|
|
314
|
+
display: flex;
|
|
315
|
+
padding-left: 1rem;
|
|
316
|
+
padding-right: 1rem;
|
|
317
|
+
align-items: flex-start;
|
|
318
|
+
justify-content: space-between;
|
|
319
|
+
width: 100%;
|
|
320
|
+
|
|
321
|
+
@media (min-width: 640px) {
|
|
322
|
+
padding-left: 1.5rem;
|
|
323
|
+
padding-right: 1.5rem;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.drawer-title {
|
|
328
|
+
line-height: 1.75rem;
|
|
329
|
+
font-weight: 600;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.drawer-main {
|
|
333
|
+
position: relative;
|
|
334
|
+
flex: 1;
|
|
335
|
+
overflow-y: auto;
|
|
336
|
+
}
|
|
337
|
+
&.drawer-container[data-padding="true"] .drawer-main {
|
|
338
|
+
margin-top: 1.5rem;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.drawer-footer {
|
|
342
|
+
display: flex;
|
|
343
|
+
flex-shrink: 0;
|
|
344
|
+
padding: 1rem;
|
|
345
|
+
|
|
346
|
+
@media (min-width: 640px) {
|
|
347
|
+
padding-left: 1.5rem;
|
|
348
|
+
padding-right: 1.5rem;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.drawer-close {
|
|
353
|
+
margin-left: 0.75rem;
|
|
354
|
+
display: flex;
|
|
355
|
+
height: 1.75rem;
|
|
356
|
+
align-items: center;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.drawer-close-button {
|
|
360
|
+
position: relative;
|
|
361
|
+
display: inline-flex;
|
|
362
|
+
padding: 0.375rem;
|
|
363
|
+
background-color: transparent;
|
|
364
|
+
border-radius: 0.375rem;
|
|
365
|
+
align-items: center;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.drawer-close-hit-area {
|
|
369
|
+
position: absolute;
|
|
370
|
+
inset: -0.625rem;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.drawer-close-icon {
|
|
374
|
+
width: 1.5rem;
|
|
375
|
+
height: 1.5rem;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/* ========================================
|
|
380
|
+
Dark / light mode (requires ancestor
|
|
381
|
+
context so must stay outside dialog.utmr)
|
|
382
|
+
======================================== */
|
|
383
|
+
|
|
384
|
+
.dark dialog.utmr {
|
|
385
|
+
&::backdrop {
|
|
386
|
+
background-color: rgba(17, 24, 39, 0.8);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.modal-header,
|
|
390
|
+
.modal-footer,
|
|
391
|
+
.drawer-header,
|
|
392
|
+
.drawer-footer {
|
|
393
|
+
border-color: #4B5563;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.modal-content,
|
|
397
|
+
.drawer-content {
|
|
398
|
+
background-color: #1F2937;
|
|
399
|
+
color: #ffffff;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.modal-close-button:hover,
|
|
403
|
+
.modal-close-icon:hover,
|
|
404
|
+
.drawer-close-button:hover,
|
|
405
|
+
.drawer-close-icon:hover {
|
|
406
|
+
background-color: #6B7280;
|
|
407
|
+
color: #ffffff;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
:not(.dark) dialog.utmr {
|
|
412
|
+
.modal-close-button,
|
|
413
|
+
.modal-close-icon,
|
|
414
|
+
.drawer-close-button,
|
|
415
|
+
.drawer-close-icon {
|
|
416
|
+
color: #9CA3AF;
|
|
417
|
+
|
|
418
|
+
&:hover {
|
|
419
|
+
color: #111827;
|
|
420
|
+
background-color: #E5E7EB;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
@@ -86,6 +86,50 @@ module UltimateTurboModal
|
|
|
86
86
|
end
|
|
87
87
|
end
|
|
88
88
|
|
|
89
|
+
# The vanilla flavor needs a stylesheet; the other flavors are styled
|
|
90
|
+
# entirely by the classes defined in their flavor file.
|
|
91
|
+
def add_vanilla_stylesheet
|
|
92
|
+
layout_path = rails_root_join("app", "views", "layouts", "application.html.erb")
|
|
93
|
+
link_tag = "<%= stylesheet_link_tag \"#{package_name}\", \"data-turbo-track\": \"reload\" %>\n"
|
|
94
|
+
|
|
95
|
+
say "\nAttempting to add the vanilla stylesheet to #{layout_path}...", :yellow
|
|
96
|
+
|
|
97
|
+
unless File.exist?(layout_path)
|
|
98
|
+
say "❌ Layout file not found at #{layout_path}.", :red
|
|
99
|
+
say " Please manually add the following line inside the <head> of your main layout:", :yellow
|
|
100
|
+
say " #{link_tag.strip}\n", :cyan
|
|
101
|
+
return
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
file_content = File.read(layout_path)
|
|
105
|
+
|
|
106
|
+
if file_content.match?(/stylesheet_link_tag\s+["']#{Regexp.escape(package_name)}["']/)
|
|
107
|
+
say "⏩ Stylesheet tag already exists.", :blue
|
|
108
|
+
return
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Insert before the first existing stylesheet_link_tag. Where the two
|
|
112
|
+
# stylesheets collide at equal specificity the later one wins, so the
|
|
113
|
+
# defaults have to load first for the app's own styles to override them.
|
|
114
|
+
existing_tag = file_content.scan(/^.*stylesheet_link_tag.*\n/).first
|
|
115
|
+
|
|
116
|
+
if existing_tag
|
|
117
|
+
insert_into_file layout_path, "#{indentation_of(existing_tag)}#{link_tag}", before: existing_tag
|
|
118
|
+
say "✅ Added stylesheet tag to the layout.", :green
|
|
119
|
+
elsif file_content.match?(%r{</head>})
|
|
120
|
+
insert_into_file layout_path, " #{link_tag}", before: %r{^\s*</head>}
|
|
121
|
+
say "✅ Added stylesheet tag before </head>.", :green
|
|
122
|
+
else
|
|
123
|
+
say "❌ Could not find a <head> section in #{layout_path}.", :red
|
|
124
|
+
say " Please manually add the following line inside the <head> of your main layout:", :yellow
|
|
125
|
+
say " #{link_tag.strip}\n", :cyan
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def indentation_of(line)
|
|
130
|
+
line[/\A[ \t]*/]
|
|
131
|
+
end
|
|
132
|
+
|
|
89
133
|
def uses_importmaps?
|
|
90
134
|
File.exist?(rails_root_join("config", "importmap.rb"))
|
|
91
135
|
end
|
|
@@ -108,6 +108,11 @@ module UltimateTurboModal
|
|
|
108
108
|
end
|
|
109
109
|
end
|
|
110
110
|
|
|
111
|
+
# Step 5: Add the stylesheet to the layout (vanilla flavor only)
|
|
112
|
+
def setup_stylesheet
|
|
113
|
+
add_vanilla_stylesheet if @framework == "vanilla"
|
|
114
|
+
end
|
|
115
|
+
|
|
111
116
|
def copy_initializer_and_flavor
|
|
112
117
|
say "\nCreating initializer for `#{@framework}` flavor...", :green
|
|
113
118
|
copy_file "ultimate_turbo_modal.rb", "config/initializers/ultimate_turbo_modal.rb"
|
|
@@ -7,6 +7,7 @@ UltimateTurboModal.configure do |config|
|
|
|
7
7
|
# config.modal do |m|
|
|
8
8
|
# m.advance = false
|
|
9
9
|
# m.close_button = true
|
|
10
|
+
# m.close_on_submit = true
|
|
10
11
|
# m.header = true
|
|
11
12
|
# m.header_divider = true
|
|
12
13
|
# m.footer_divider = true
|
|
@@ -16,7 +17,9 @@ UltimateTurboModal.configure do |config|
|
|
|
16
17
|
|
|
17
18
|
# config.drawer do |d|
|
|
18
19
|
# d.position = :right
|
|
20
|
+
# d.advance = false
|
|
19
21
|
# d.close_button = true
|
|
22
|
+
# d.close_on_submit = true
|
|
20
23
|
# d.header = true
|
|
21
24
|
# d.header_divider = false
|
|
22
25
|
# d.footer_divider = true
|
|
@@ -7,11 +7,12 @@ class UltimateTurboModal::Base < Phlex::HTML
|
|
|
7
7
|
VALID_DRAWER_SIZES = %i[xs sm md lg xl 2xl full].freeze
|
|
8
8
|
VALID_DRAWER_POSITIONS = %i[right left].freeze
|
|
9
9
|
|
|
10
|
-
# @param advance [Boolean, String] Whether to update the browser history when opening and closing the modal
|
|
10
|
+
# @param advance [Boolean, String] Whether to update the browser history when opening and closing the modal or drawer
|
|
11
11
|
# @param allowed_click_outside_selector [String] CSS selectors for elements that are allowed to be clicked outside of the modal without dismissing the modal
|
|
12
12
|
# @param close_button [Boolean] Whether to show a close button
|
|
13
13
|
# @param close_button_data_action [String] `data-action` attribute for the close button
|
|
14
14
|
# @param close_button_sr_label [String] Close button label for screen readers
|
|
15
|
+
# @param close_on_submit [Boolean] Whether to dismiss on a successful, non-redirecting form submission
|
|
15
16
|
# @param drawer_position [Symbol, false] Internal: drawer position (:right, :left) or false for standard modal. Use the `drawer()` view helper instead.
|
|
16
17
|
# @param footer_divider [Boolean] Whether to show a divider between the main content and the footer
|
|
17
18
|
# @param header [Boolean] Whether to show a modal header
|
|
@@ -28,6 +29,7 @@ class UltimateTurboModal::Base < Phlex::HTML
|
|
|
28
29
|
close_button: nil,
|
|
29
30
|
close_button_data_action: "modal#hideModal",
|
|
30
31
|
close_button_sr_label: "Close modal",
|
|
32
|
+
close_on_submit: nil,
|
|
31
33
|
drawer_position: false,
|
|
32
34
|
footer_divider: nil,
|
|
33
35
|
header: nil,
|
|
@@ -45,18 +47,16 @@ class UltimateTurboModal::Base < Phlex::HTML
|
|
|
45
47
|
|
|
46
48
|
if drawer?
|
|
47
49
|
cfg = UltimateTurboModal.configuration.drawer_config
|
|
48
|
-
@advance = false
|
|
49
|
-
@advance_url = nil
|
|
50
|
-
@close_button = close_button.nil? ? cfg.close_button : close_button
|
|
51
50
|
@drawer_size = self.class.validate_drawer_size!(size || cfg.size)
|
|
52
51
|
else
|
|
53
52
|
cfg = UltimateTurboModal.configuration.modal_config
|
|
54
|
-
adv = advance.nil? ? cfg.advance : advance
|
|
55
|
-
@advance = !!adv
|
|
56
|
-
@advance_url = (adv.present? && adv.is_a?(String)) ? adv : nil
|
|
57
|
-
@close_button = close_button.nil? ? cfg.close_button : close_button
|
|
58
53
|
@drawer_size = nil
|
|
59
54
|
end
|
|
55
|
+
adv = advance.nil? ? cfg.advance : advance
|
|
56
|
+
@advance = !!adv
|
|
57
|
+
@advance_url = (adv.present? && adv.is_a?(String)) ? adv : nil
|
|
58
|
+
@close_button = close_button.nil? ? cfg.close_button : close_button
|
|
59
|
+
@close_on_submit = close_on_submit.nil? ? cfg.close_on_submit : close_on_submit
|
|
60
60
|
@footer_divider = footer_divider.nil? ? cfg.footer_divider : footer_divider
|
|
61
61
|
@header = header.nil? ? cfg.header : header
|
|
62
62
|
@header_divider = header_divider.nil? ? cfg.header_divider : header_divider
|
|
@@ -134,6 +134,8 @@ class UltimateTurboModal::Base < Phlex::HTML
|
|
|
134
134
|
|
|
135
135
|
def close_button? = !!@close_button
|
|
136
136
|
|
|
137
|
+
def close_on_submit? = !!@close_on_submit
|
|
138
|
+
|
|
137
139
|
def title_block? = !!@title_block
|
|
138
140
|
|
|
139
141
|
def title? = !!@title
|
|
@@ -265,6 +267,7 @@ class UltimateTurboModal::Base < Phlex::HTML
|
|
|
265
267
|
modal_target: "container",
|
|
266
268
|
modal_advance_url_value: advance_url,
|
|
267
269
|
modal_allowed_click_outside_selector_value: allowed_click_outside_selector,
|
|
270
|
+
modal_close_on_submit_value: close_on_submit?.to_s,
|
|
268
271
|
action: "turbo:submit-end->modal#submitEnd cancel->modal#cancelEvent mousedown->modal#dialogMousedown click->modal#dialogClicked",
|
|
269
272
|
padding: padding?.to_s,
|
|
270
273
|
title: title?.to_s,
|
|
@@ -41,7 +41,7 @@ module UltimateTurboModal
|
|
|
41
41
|
|
|
42
42
|
# Shared base for modal and drawer configuration
|
|
43
43
|
class BaseConfig
|
|
44
|
-
attr_reader :close_button, :header, :header_divider, :footer_divider, :padding, :overlay
|
|
44
|
+
attr_reader :advance, :close_button, :close_on_submit, :header, :header_divider, :footer_divider, :padding, :overlay
|
|
45
45
|
|
|
46
46
|
def self.boolean_option(name)
|
|
47
47
|
define_method(:"#{name}=") do |value|
|
|
@@ -51,11 +51,20 @@ module UltimateTurboModal
|
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
boolean_option :close_button
|
|
54
|
+
boolean_option :close_on_submit
|
|
54
55
|
boolean_option :header
|
|
55
56
|
boolean_option :header_divider
|
|
56
57
|
boolean_option :footer_divider
|
|
57
58
|
boolean_option :overlay
|
|
58
59
|
|
|
60
|
+
def advance=(value)
|
|
61
|
+
if [true, false].include?(value) || value.is_a?(String)
|
|
62
|
+
@advance = value
|
|
63
|
+
else
|
|
64
|
+
raise ArgumentError, "Value must be a boolean or a String."
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
59
68
|
def padding=(padding)
|
|
60
69
|
if [true, false].include?(padding) || padding.is_a?(String)
|
|
61
70
|
@padding = padding
|
|
@@ -66,26 +75,25 @@ module UltimateTurboModal
|
|
|
66
75
|
end
|
|
67
76
|
|
|
68
77
|
class ModalConfig < BaseConfig
|
|
69
|
-
attr_reader :advance
|
|
70
|
-
|
|
71
78
|
def initialize
|
|
72
79
|
@advance = false
|
|
73
80
|
@close_button = true
|
|
81
|
+
@close_on_submit = true
|
|
74
82
|
@header = true
|
|
75
83
|
@header_divider = true
|
|
76
84
|
@footer_divider = true
|
|
77
85
|
@padding = true
|
|
78
86
|
@overlay = true
|
|
79
87
|
end
|
|
80
|
-
|
|
81
|
-
boolean_option :advance
|
|
82
88
|
end
|
|
83
89
|
|
|
84
90
|
class DrawerConfig < BaseConfig
|
|
85
91
|
attr_reader :size, :position
|
|
86
92
|
|
|
87
93
|
def initialize
|
|
94
|
+
@advance = false
|
|
88
95
|
@close_button = true
|
|
96
|
+
@close_on_submit = true
|
|
89
97
|
@header = true
|
|
90
98
|
@header_divider = false
|
|
91
99
|
@footer_divider = true
|
|
@@ -8,6 +8,29 @@ require "ultimate_turbo_modal/helpers/stream_helper"
|
|
|
8
8
|
|
|
9
9
|
module UltimateTurboModal
|
|
10
10
|
class Railtie < Rails::Railtie
|
|
11
|
+
# Absolute path to the stylesheets shipped with the gem. Exposed so that
|
|
12
|
+
# apps and tooling can locate `ultimate_turbo_modal.css` without guessing.
|
|
13
|
+
def self.stylesheets_path
|
|
14
|
+
File.expand_path("../../app/assets/stylesheets", __dir__)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Make `ultimate_turbo_modal.css` available to the asset pipeline so that
|
|
18
|
+
# `stylesheet_link_tag "ultimate_turbo_modal"` works under Propshaft and
|
|
19
|
+
# Sprockets alike, regardless of whether the app uses importmaps or a
|
|
20
|
+
# JavaScript bundler.
|
|
21
|
+
initializer "ultimate_turbo_modal.assets" do |app|
|
|
22
|
+
next unless app.config.respond_to?(:assets)
|
|
23
|
+
next unless app.config.assets.respond_to?(:paths)
|
|
24
|
+
|
|
25
|
+
app.config.assets.paths << UltimateTurboModal::Railtie.stylesheets_path
|
|
26
|
+
|
|
27
|
+
# Sprockets only precompiles what it is told about; Propshaft compiles
|
|
28
|
+
# everything on its load path and ignores this list.
|
|
29
|
+
if defined?(Sprockets) && app.config.assets.respond_to?(:precompile)
|
|
30
|
+
app.config.assets.precompile << "ultimate_turbo_modal.css"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
11
34
|
initializer "ultimate_turbo_modal.action_controller" do
|
|
12
35
|
ActiveSupport.on_load(:action_controller_base) do
|
|
13
36
|
include UltimateTurboModal::Helpers::ControllerHelper
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ultimate_turbo_modal
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Carl Mercier
|
|
@@ -139,6 +139,7 @@ files:
|
|
|
139
139
|
- Rakefile
|
|
140
140
|
- UPGRADING.md
|
|
141
141
|
- VERSION
|
|
142
|
+
- app/assets/stylesheets/ultimate_turbo_modal.css
|
|
142
143
|
- lefthook.yml
|
|
143
144
|
- lib/generators/ultimate_turbo_modal/base.rb
|
|
144
145
|
- lib/generators/ultimate_turbo_modal/install_generator.rb
|