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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f0c59204e8c87cee87fc85083ef4147930a7a493b5d9c6a8ddf7e1ac76f04359
4
- data.tar.gz: 92c975e4676ec8ae2ae953be97f6beea70d2aa3d90b6403564af08950710a058
3
+ metadata.gz: b52125720fe85438df47417fd9fc02a2d9509d3da42d1abbbe392892f5c0cc8a
4
+ data.tar.gz: 67bad39477deb507e85a4fd2b481f4b7947c8eda4391c10c9d392d0935c50b85
5
5
  SHA512:
6
- metadata.gz: 918fac0dfe7519413443eac794e82b31d8014ecdbaae3279baeb0474fa00f2445b59c57f249ff35f7e5bcc0ab6c3dd6f91fecdaf3e30e34c78326b2d02e25082
7
- data.tar.gz: 6cf8a3e810da824c8bb2cf6eb1f5c4fc9ef75671dbae67486289a927599c2c3984fa5d046fa2ac17562e78db63635145016bb4a0f1d46441b26e44c7b2d44b53
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ultimate_turbo_modal (3.2.1)
4
+ ultimate_turbo_modal (3.3.0)
5
5
  actionpack (>= 8.0)
6
6
  activesupport (>= 8.0)
7
7
  phlex-rails (>= 2.0)
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.2.1
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
@@ -76,6 +76,8 @@ module UltimateTurboModal
76
76
  return
77
77
  end
78
78
 
79
+ add_vanilla_stylesheet if flavor == "vanilla"
80
+
79
81
  template_rel = "flavors/#{flavor}.rb"
80
82
  template_abs = File.join(self.class.source_root, template_rel)
81
83
 
@@ -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 (modal-only, ignored for drawers)
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.2.1
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