ultimate_turbo_modal 3.3.0 → 3.4.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 +5 -0
- data/Gemfile.lock +1 -1
- data/README.md +163 -0
- data/VERSION +1 -1
- data/app/assets/stylesheets/ultimate_turbo_modal.css +187 -0
- data/lib/generators/ultimate_turbo_modal/install_generator.rb +3 -0
- data/lib/generators/ultimate_turbo_modal/templates/flavors/custom.rb +10 -1
- data/lib/generators/ultimate_turbo_modal/templates/flavors/tailwind.rb +78 -4
- data/lib/generators/ultimate_turbo_modal/templates/flavors/vanilla.rb +14 -1
- data/lib/generators/ultimate_turbo_modal/templates/ultimate_turbo_modal.rb +15 -0
- data/lib/ultimate_turbo_modal/base.rb +162 -19
- data/lib/ultimate_turbo_modal/configuration.rb +33 -1
- data/lib/ultimate_turbo_modal/helpers/view_helper.rb +31 -0
- data/lib/ultimate_turbo_modal.rb +4 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e8449cf081c6d1dfc2b84106cc16b316fb6a15b25031a2a9dc94c2ce4bf99b35
|
|
4
|
+
data.tar.gz: b1067b8a6ef21a15e9556bee1e8a0ceb45fdb453ff8fa772eda0ad5191b7d669
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 86b485906013944b475e4f0aece1ee980dd226aa160b8daed3628fb84b8b7cbbea8b015086310372f2fceab89d34372a18acd0b4cd76720dbf5af0cd18574f74
|
|
7
|
+
data.tar.gz: 5e6753fa5decf7401e07a2bda70dcef18a25fe56c2c487f0eb76e66dc010b556b52a2d7893201de73815431bd95600a80861de94e7758d863d3e98949d67783f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [3.4.0] - 2026-09-10
|
|
4
|
+
|
|
5
|
+
- Added optional Turbo Confirm support: `data-turbo-confirm` prompts can now render as a UTMR dialog instead of the browser's `window.confirm`.
|
|
6
|
+
- Fixed `<%= m.title do %>` and `<%= m.footer do %>` printing a `Proc` into the modal body. Both forms now work, with or without the `=`.
|
|
7
|
+
|
|
3
8
|
## [3.3.0] - 2026-09-05
|
|
4
9
|
|
|
5
10
|
- 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`.
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -146,6 +146,14 @@ UltimateTurboModal.configure do |config|
|
|
|
146
146
|
d.overlay = true
|
|
147
147
|
d.size = :md
|
|
148
148
|
end
|
|
149
|
+
|
|
150
|
+
# Only used when Turbo Confirm support is enabled.
|
|
151
|
+
# See https://github.com/cmer/ultimate_turbo_modal#turbo-confirm
|
|
152
|
+
config.confirm do |c|
|
|
153
|
+
c.title = "Are you sure?"
|
|
154
|
+
c.accept_label = "OK"
|
|
155
|
+
c.cancel_label = "Cancel"
|
|
156
|
+
end
|
|
149
157
|
end
|
|
150
158
|
```
|
|
151
159
|
|
|
@@ -288,6 +296,160 @@ inside a default modal keeps that one form from dismissing it. Placing the
|
|
|
288
296
|
attribute on a wrapping element applies it to every form inside; the nearest
|
|
289
297
|
one wins.
|
|
290
298
|
|
|
299
|
+
## Turbo Confirm
|
|
300
|
+
|
|
301
|
+
Optional, opt-in. Once enabled, Turbo's `data-turbo-confirm` prompts render as
|
|
302
|
+
UTMR dialogs in your app's flavor instead of the browser's `window.confirm`.
|
|
303
|
+
|
|
304
|
+
Enabling it is one line in your layout:
|
|
305
|
+
|
|
306
|
+
```erb
|
|
307
|
+
<%= modal_confirm_template %>
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
That's the whole opt-in — no JavaScript changes. The helper renders an inert
|
|
311
|
+
`<template>`, and its presence on the page is the switch: with it, UTMR handles
|
|
312
|
+
confirmations; without it, Turbo falls back to `window.confirm` exactly as
|
|
313
|
+
before. Remove the line to turn the feature off.
|
|
314
|
+
|
|
315
|
+
You can also flip it from the initializer, which is handy for toggling per
|
|
316
|
+
environment without touching the layout:
|
|
317
|
+
|
|
318
|
+
```ruby
|
|
319
|
+
UltimateTurboModal.configure do |config|
|
|
320
|
+
config.confirm do |c|
|
|
321
|
+
c.enabled = false
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### Basic usage
|
|
327
|
+
|
|
328
|
+
Nothing changes about how you write confirmations:
|
|
329
|
+
|
|
330
|
+
```erb
|
|
331
|
+
<%= button_to "Delete", post_path(post), method: :delete,
|
|
332
|
+
form: { data: { turbo_confirm: "This can't be undone." } } %>
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
The message becomes the dialog body; the title and button labels come from your
|
|
336
|
+
configured defaults.
|
|
337
|
+
|
|
338
|
+
> [!NOTE]
|
|
339
|
+
> Turbo only runs confirmations for form submissions. A plain `<a>` needs
|
|
340
|
+
> `data-turbo-method` (or `data-turbo-stream`) for `data-turbo-confirm` to fire
|
|
341
|
+
> at all — that is Turbo's behavior, not UTMR's.
|
|
342
|
+
|
|
343
|
+
### Customizing a single confirmation
|
|
344
|
+
|
|
345
|
+
For anything beyond the message, use the `modal_confirm` helper. It builds the
|
|
346
|
+
`data` attributes for you:
|
|
347
|
+
|
|
348
|
+
```erb
|
|
349
|
+
<%= link_to "Delete", post_path(post), data: modal_confirm(
|
|
350
|
+
"This can't be undone.",
|
|
351
|
+
title: "Delete post?",
|
|
352
|
+
accept: "Delete",
|
|
353
|
+
variant: :danger,
|
|
354
|
+
turbo_method: :delete) %>
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
| Option | Description |
|
|
358
|
+
|--------|-------------|
|
|
359
|
+
| `body` | First positional argument. The message. |
|
|
360
|
+
| `title` | Dialog heading. |
|
|
361
|
+
| `accept` | Label for the confirming button. |
|
|
362
|
+
| `cancel` | Label for the dismissing button. |
|
|
363
|
+
| `variant` | `:danger` styles the accept button destructively and moves the initial focus to Cancel. |
|
|
364
|
+
| `native` | `true` uses the browser's own `window.confirm` for this one prompt. |
|
|
365
|
+
|
|
366
|
+
Any option you leave out keeps its configured default.
|
|
367
|
+
|
|
368
|
+
You can also write the attributes by hand, which is convenient on forms:
|
|
369
|
+
|
|
370
|
+
```erb
|
|
371
|
+
<%= button_to "Delete", post_path(post), method: :delete, form: { data: {
|
|
372
|
+
turbo_confirm: "This can't be undone.",
|
|
373
|
+
turbo_confirm_title: "Delete post?",
|
|
374
|
+
turbo_confirm_accept: "Delete",
|
|
375
|
+
turbo_confirm_variant: "danger" } } %>
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
`data-turbo-confirm` itself has to be on the form or on the submit button:
|
|
379
|
+
Turbo looks nowhere else, and a prompt written on a wrapping element is simply
|
|
380
|
+
never triggered. The `data-turbo-confirm-*` options are read once the prompt has
|
|
381
|
+
fired, so those may also sit on a wrapping element. The submitter wins when both
|
|
382
|
+
it and the form carry the same option.
|
|
383
|
+
|
|
384
|
+
> [!IMPORTANT]
|
|
385
|
+
> On a **link**, use `modal_confirm`. Sibling `data-turbo-confirm-*` attributes
|
|
386
|
+
> do not survive: Turbo rewrites a link carrying `data-turbo-method` into a
|
|
387
|
+
> hidden form and copies only a fixed set of attributes across, so they are gone
|
|
388
|
+
> before UTMR is called. The helper's JSON payload rides inside
|
|
389
|
+
> `data-turbo-confirm` itself, which always survives. On forms, either style
|
|
390
|
+
> works.
|
|
391
|
+
|
|
392
|
+
### Defaults
|
|
393
|
+
|
|
394
|
+
```ruby
|
|
395
|
+
UltimateTurboModal.configure do |config|
|
|
396
|
+
config.confirm do |c|
|
|
397
|
+
c.enabled = true
|
|
398
|
+
c.title = "Are you sure?"
|
|
399
|
+
c.accept_label = "OK"
|
|
400
|
+
c.cancel_label = "Cancel"
|
|
401
|
+
c.close_button = false # a confirm has its own Cancel button
|
|
402
|
+
c.header_divider = false # dividers chop up one or two lines of text
|
|
403
|
+
c.footer_divider = false
|
|
404
|
+
c.header = true
|
|
405
|
+
c.padding = true
|
|
406
|
+
c.overlay = true
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
The confirm dialog is deliberately plainer than a modal: no close button and no
|
|
412
|
+
dividers by default, and a set width (floor `20rem`, cap `28rem`) so a one-word
|
|
413
|
+
prompt and a three-line one come out the same size. Adjust it by overriding
|
|
414
|
+
`CONFIRM_CONTENT_CLASSES` in your flavor file.
|
|
415
|
+
|
|
416
|
+
### Behavior
|
|
417
|
+
|
|
418
|
+
- The dialog is appended to `<body>`, so it layers above an open modal or drawer without any extra setup.
|
|
419
|
+
- **ESC** and the Cancel button both cancel — the action does not run.
|
|
420
|
+
- Clicking the backdrop does nothing. A confirm has to be answered, so a stray click never decides it.
|
|
421
|
+
- The confirming button is focused on open, so Enter accepts. `variant: :danger` focuses Cancel instead.
|
|
422
|
+
- Accepting waits for the close animation to finish before the request is sent.
|
|
423
|
+
- `window.modal` keeps pointing at the modal underneath, so `turbo_stream.modal(:close)` still addresses the right dialog.
|
|
424
|
+
- `data-turbo-confirm-native` on a form or submitter opts that one confirmation back out to `window.confirm`. On a link, pass `native: true` to `modal_confirm` instead, for the same reason the other options have to travel in the payload.
|
|
425
|
+
- If something else in your app assigns `Turbo.config.forms.confirm` after UTMR loads, it wins. Import `enableModalConfirm` from the package and call it afterwards to take the hook back.
|
|
426
|
+
|
|
427
|
+
### Styling
|
|
428
|
+
|
|
429
|
+
The dialog is cloned from the template, so it shares your flavor's dialog,
|
|
430
|
+
backdrop and transition classes. Every slot falls back to its `MODAL_*` class
|
|
431
|
+
unless the flavor defines a `CONFIRM_*` override, so you only need to define
|
|
432
|
+
what should differ:
|
|
433
|
+
|
|
434
|
+
| Constant | Falls back to |
|
|
435
|
+
|----------|---------------|
|
|
436
|
+
| `CONFIRM_INNER_CLASSES` | `MODAL_INNER_CLASSES` |
|
|
437
|
+
| `CONFIRM_CONTENT_CLASSES` | `MODAL_CONTENT_CLASSES` |
|
|
438
|
+
| `CONFIRM_HEADER_CLASSES` | `MODAL_HEADER_CLASSES` |
|
|
439
|
+
| `CONFIRM_TITLE_CLASSES` | `MODAL_TITLE_CLASSES` |
|
|
440
|
+
| `CONFIRM_TITLE_H_CLASSES` | `MODAL_TITLE_H_CLASSES` |
|
|
441
|
+
| `CONFIRM_MAIN_CLASSES` | `MODAL_MAIN_CLASSES` |
|
|
442
|
+
| `CONFIRM_FOOTER_CLASSES` | `MODAL_FOOTER_CLASSES` |
|
|
443
|
+
| `CONFIRM_BODY_CLASSES` | — (confirm only) |
|
|
444
|
+
| `CONFIRM_ACTIONS_CLASSES` | — (confirm only) |
|
|
445
|
+
| `CONFIRM_ACCEPT_CLASSES` | — (confirm only) |
|
|
446
|
+
| `CONFIRM_CANCEL_CLASSES` | — (confirm only) |
|
|
447
|
+
|
|
448
|
+
Upgrading from an earlier version? Run
|
|
449
|
+
`rails generate ultimate_turbo_modal:update` to refresh your flavor file — until
|
|
450
|
+
you do, the confirm dialog falls back to the modal's classes and the four
|
|
451
|
+
confirm-only slots render unstyled rather than raising.
|
|
452
|
+
|
|
291
453
|
## Opening a Modal from a Drawer
|
|
292
454
|
|
|
293
455
|
You don't need to do anything special. Use `data-turbo-frame="modal"` like you would anywhere else, and UTMR handles the rest:
|
|
@@ -347,6 +509,7 @@ For a full lifecycle walkthrough and edge-case notes, see [docs/modal-from-drawe
|
|
|
347
509
|
- Automatic (or not) close button
|
|
348
510
|
- Native focus trapping via the `<dialog>` element for improved accessibility (Tab and Shift+Tab cycle through focusable elements within the modal only)
|
|
349
511
|
- Smooth redirects: form submissions that redirect back to the same page morph the content behind the modal before closing; redirects to a different page close the modal with animation first, then navigate
|
|
512
|
+
- Optional Turbo Confirm support: `data-turbo-confirm` prompts render as a styled dialog instead of the browser's `window.confirm`
|
|
350
513
|
|
|
351
514
|
|
|
352
515
|
### Running the Demo Application
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.4.0
|
|
@@ -190,6 +190,160 @@ dialog.utmr {
|
|
|
190
190
|
height: 1.25rem;
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
/* ========================================
|
|
194
|
+
Confirm dialog styles
|
|
195
|
+
Scoped to [data-utmr-confirm] so it can reuse the modal container,
|
|
196
|
+
backdrop and enter/leave transitions while overriding the layout.
|
|
197
|
+
======================================== */
|
|
198
|
+
|
|
199
|
+
&[data-utmr-confirm] {
|
|
200
|
+
/* Real gutter on mobile: the card below is full width. */
|
|
201
|
+
.modal-inner {
|
|
202
|
+
padding: 1rem;
|
|
203
|
+
padding-top: 10vh;
|
|
204
|
+
|
|
205
|
+
@media (min-width: 640px) {
|
|
206
|
+
padding: 1rem;
|
|
207
|
+
padding-bottom: 10vh;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/* Sized rather than shrink-to-fit, so a one-word prompt and a three-line
|
|
212
|
+
one come out the same. The floor is written as min(20rem, 100%) so it can
|
|
213
|
+
never push the card wider than the space it has on a narrow screen. */
|
|
214
|
+
.confirm-content {
|
|
215
|
+
position: relative;
|
|
216
|
+
overflow: hidden;
|
|
217
|
+
width: 100%;
|
|
218
|
+
min-width: min(20rem, 100%);
|
|
219
|
+
background-color: #ffffff;
|
|
220
|
+
text-align: left;
|
|
221
|
+
border-radius: 0.5rem;
|
|
222
|
+
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
|
|
223
|
+
|
|
224
|
+
@media (min-width: 640px) {
|
|
225
|
+
max-width: 28rem;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.confirm-header {
|
|
230
|
+
display: flex;
|
|
231
|
+
align-items: flex-start;
|
|
232
|
+
justify-content: space-between;
|
|
233
|
+
gap: 1rem;
|
|
234
|
+
width: 100%;
|
|
235
|
+
padding: 1.5rem 1.5rem 0;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.confirm-title-h {
|
|
239
|
+
font-size: 1rem;
|
|
240
|
+
line-height: 1.5rem;
|
|
241
|
+
font-weight: 600;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.confirm-main {
|
|
245
|
+
padding: 0;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.confirm-body {
|
|
249
|
+
font-size: 0.875rem;
|
|
250
|
+
line-height: 1.5rem;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.confirm-footer {
|
|
254
|
+
padding: 1.5rem;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/* Stacked and full width on mobile, inline and right aligned from 640px.
|
|
258
|
+
Reversed so the primary action sits on top. */
|
|
259
|
+
.confirm-actions {
|
|
260
|
+
display: flex;
|
|
261
|
+
width: 100%;
|
|
262
|
+
flex-direction: column-reverse;
|
|
263
|
+
gap: 0.5rem;
|
|
264
|
+
|
|
265
|
+
@media (min-width: 640px) {
|
|
266
|
+
flex-direction: row;
|
|
267
|
+
justify-content: flex-end;
|
|
268
|
+
gap: 0.75rem;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.confirm-cancel,
|
|
273
|
+
.confirm-accept {
|
|
274
|
+
display: inline-flex;
|
|
275
|
+
width: 100%;
|
|
276
|
+
justify-content: center;
|
|
277
|
+
align-items: center;
|
|
278
|
+
padding: 0.625rem 1rem;
|
|
279
|
+
font-size: 0.875rem;
|
|
280
|
+
line-height: 1.25rem;
|
|
281
|
+
font-weight: 600;
|
|
282
|
+
border-radius: 0.375rem;
|
|
283
|
+
cursor: pointer;
|
|
284
|
+
transition: background-color 150ms ease-in-out;
|
|
285
|
+
|
|
286
|
+
@media (min-width: 640px) {
|
|
287
|
+
width: auto;
|
|
288
|
+
padding: 0.5rem 1rem;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.confirm-cancel {
|
|
293
|
+
background-color: #ffffff;
|
|
294
|
+
color: #111827;
|
|
295
|
+
border: 1px solid #D1D5DB;
|
|
296
|
+
|
|
297
|
+
&:hover {
|
|
298
|
+
background-color: #F9FAFB;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.confirm-accept {
|
|
303
|
+
background-color: #4F46E5;
|
|
304
|
+
color: #ffffff;
|
|
305
|
+
border: 1px solid transparent;
|
|
306
|
+
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
|
307
|
+
|
|
308
|
+
&:hover {
|
|
309
|
+
background-color: #6366F1;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/* Destructive variant, set from `variant: :danger` */
|
|
314
|
+
&[data-utmr-confirm-variant="danger"] .confirm-accept {
|
|
315
|
+
background-color: #DC2626;
|
|
316
|
+
|
|
317
|
+
&:hover {
|
|
318
|
+
background-color: #EF4444;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/* Options, matching the modal's data-attribute switches */
|
|
323
|
+
&[data-header="false"] .confirm-header {
|
|
324
|
+
display: none;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
&[data-title="false"] .confirm-title-h {
|
|
328
|
+
display: none;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
&[data-padding="true"] .confirm-main {
|
|
332
|
+
padding: 0.5rem 1.5rem 0;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
&[data-header-divider="true"] .confirm-header {
|
|
336
|
+
padding-bottom: 1rem;
|
|
337
|
+
border-bottom-width: 1px;
|
|
338
|
+
border-bottom-style: solid;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
&[data-footer-divider="true"] .confirm-footer {
|
|
342
|
+
border-top-width: 1px;
|
|
343
|
+
border-top-style: solid;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
193
347
|
/* ========================================
|
|
194
348
|
Drawer styles
|
|
195
349
|
======================================== */
|
|
@@ -399,6 +553,30 @@ dialog.utmr {
|
|
|
399
553
|
color: #ffffff;
|
|
400
554
|
}
|
|
401
555
|
|
|
556
|
+
.confirm-content {
|
|
557
|
+
background-color: #1F2937;
|
|
558
|
+
color: #ffffff;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
.confirm-body {
|
|
562
|
+
color: #9CA3AF;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
.confirm-header,
|
|
566
|
+
.confirm-footer {
|
|
567
|
+
border-color: #4B5563;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
.confirm-cancel {
|
|
571
|
+
background-color: #374151;
|
|
572
|
+
color: #F3F4F6;
|
|
573
|
+
border-color: #4B5563;
|
|
574
|
+
|
|
575
|
+
&:hover {
|
|
576
|
+
background-color: #4B5563;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
402
580
|
.modal-close-button:hover,
|
|
403
581
|
.modal-close-icon:hover,
|
|
404
582
|
.drawer-close-button:hover,
|
|
@@ -409,6 +587,15 @@ dialog.utmr {
|
|
|
409
587
|
}
|
|
410
588
|
|
|
411
589
|
:not(.dark) dialog.utmr {
|
|
590
|
+
.confirm-body {
|
|
591
|
+
color: #6B7280;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
.confirm-header,
|
|
595
|
+
.confirm-footer {
|
|
596
|
+
border-color: #E5E7EB;
|
|
597
|
+
}
|
|
598
|
+
|
|
412
599
|
.modal-close-button,
|
|
413
600
|
.modal-close-icon,
|
|
414
601
|
.drawer-close-button,
|
|
@@ -127,6 +127,9 @@ module UltimateTurboModal
|
|
|
127
127
|
def show_readme
|
|
128
128
|
say "\nUltimateTurboModal installation complete!\n", :magenta
|
|
129
129
|
say "Please review the initializer files, ensure JS is set up, and check your layout file.", :magenta
|
|
130
|
+
say "\nOptional: to render `data-turbo-confirm` prompts as modals instead of", :magenta
|
|
131
|
+
say "the browser's confirm dialog, add this to your layout:", :magenta
|
|
132
|
+
say " <%= modal_confirm_template %>", :cyan
|
|
130
133
|
say "Don't forget to restart your Rails server!", :yellow
|
|
131
134
|
end
|
|
132
135
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
# Set to "" or omit entirely if you handle all styling via classes and external CSS.
|
|
9
9
|
module UltimateTurboModal::Flavors
|
|
10
10
|
class Custom < UltimateTurboModal::Base
|
|
11
|
-
STYLES = "html:has(dialog
|
|
11
|
+
STYLES = "html:has(dialog.utmr[open]) { overflow: hidden; }"
|
|
12
12
|
|
|
13
13
|
MODAL_DIALOG_CLASSES = ""
|
|
14
14
|
MODAL_INNER_CLASSES = ""
|
|
@@ -23,6 +23,15 @@ module UltimateTurboModal::Flavors
|
|
|
23
23
|
MODAL_CLOSE_SR_CLASSES = ""
|
|
24
24
|
MODAL_CLOSE_ICON_CLASSES = ""
|
|
25
25
|
|
|
26
|
+
# Confirm dialog constants
|
|
27
|
+
# Only the slots that have no modal equivalent are listed. Every other slot
|
|
28
|
+
# (dialog, inner, content, header, title, main, footer) falls back to the
|
|
29
|
+
# matching MODAL_* class above; define a CONFIRM_* one to override it.
|
|
30
|
+
CONFIRM_BODY_CLASSES = ""
|
|
31
|
+
CONFIRM_ACTIONS_CLASSES = ""
|
|
32
|
+
CONFIRM_CANCEL_CLASSES = ""
|
|
33
|
+
CONFIRM_ACCEPT_CLASSES = ""
|
|
34
|
+
|
|
26
35
|
# Drawer constants
|
|
27
36
|
DRAWER_DIALOG_CLASSES = MODAL_DIALOG_CLASSES
|
|
28
37
|
DRAWER_WRAPPER_CLASSES = ""
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
# `data-entered` state (which would prevent the modal from animating out).
|
|
9
9
|
module UltimateTurboModal::Flavors
|
|
10
10
|
class Tailwind < UltimateTurboModal::Base
|
|
11
|
-
STYLES = "html:has(dialog
|
|
11
|
+
STYLES = "html:has(dialog.utmr[open]) { overflow: hidden; }"
|
|
12
12
|
|
|
13
13
|
# Modal constants
|
|
14
14
|
|
|
@@ -25,9 +25,11 @@ module UltimateTurboModal::Flavors
|
|
|
25
25
|
"data-[closing]:backdrop:duration-200 data-[closing]:backdrop:ease-in"
|
|
26
26
|
].join(" ")
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
# The enter/leave animation, shared by the modal and the confirm dialog.
|
|
29
|
+
# Tailwind resolves conflicts by CSS source order rather than by the order
|
|
30
|
+
# classes appear in the string, so the two cannot share a layout line -- but
|
|
31
|
+
# they must not drift apart on the animation.
|
|
32
|
+
MODAL_TRANSITION_CLASSES = [
|
|
31
33
|
"transition duration-300 ease-out",
|
|
32
34
|
"group-data-[closing]/utmr-modal:duration-200 group-data-[closing]/utmr-modal:ease-in",
|
|
33
35
|
# Default state (closed): faded + shifted
|
|
@@ -36,6 +38,11 @@ module UltimateTurboModal::Flavors
|
|
|
36
38
|
"group-data-[entered]/utmr-modal:opacity-100 group-data-[entered]/utmr-modal:translate-y-0 group-data-[entered]/utmr-modal:scale-100"
|
|
37
39
|
].join(" ")
|
|
38
40
|
|
|
41
|
+
MODAL_INNER_CLASSES = [
|
|
42
|
+
"flex min-h-full items-start justify-center pt-[10vh] sm:items-center sm:pt-0 sm:p-4 sm:pb-[10vh]",
|
|
43
|
+
MODAL_TRANSITION_CLASSES
|
|
44
|
+
].join(" ")
|
|
45
|
+
|
|
39
46
|
MODAL_CONTENT_CLASSES = "relative transform max-h-screen overflow-hidden rounded-lg bg-white text-left shadow-lg transition-all sm:max-w-3xl dark:bg-gray-800 dark:text-white"
|
|
40
47
|
MODAL_MAIN_CLASSES = "group-data-[padding=true]/utmr-modal:p-4 group-data-[padding=true]/utmr-modal:pt-2 overflow-y-auto max-h-[75vh]"
|
|
41
48
|
MODAL_HEADER_CLASSES = "flex justify-between items-center w-full py-4 rounded-t dark:border-gray-600 group-data-[header-divider=true]/utmr-modal:border-b group-data-[header=false]/utmr-modal:absolute"
|
|
@@ -47,6 +54,73 @@ module UltimateTurboModal::Flavors
|
|
|
47
54
|
MODAL_CLOSE_BUTTON_CLASSES = "text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white"
|
|
48
55
|
MODAL_CLOSE_ICON_CLASSES = "w-5 h-5"
|
|
49
56
|
|
|
57
|
+
# Confirm dialog constants
|
|
58
|
+
#
|
|
59
|
+
# The confirm dialog renders through the modal's markup, so it inherits the
|
|
60
|
+
# dialog, backdrop and transition classes above. These constants only
|
|
61
|
+
# narrow the card and retune the spacing for what is a short prompt rather
|
|
62
|
+
# than a page of content. Any slot left undefined falls back to MODAL_*.
|
|
63
|
+
|
|
64
|
+
# Same transition as a modal, with a real gutter so the full-width card
|
|
65
|
+
# never touches the screen edge on mobile.
|
|
66
|
+
CONFIRM_INNER_CLASSES = [
|
|
67
|
+
"flex min-h-full items-start justify-center p-4 pt-[10vh] sm:items-center sm:pt-4 sm:pb-[10vh]",
|
|
68
|
+
MODAL_TRANSITION_CLASSES
|
|
69
|
+
].join(" ")
|
|
70
|
+
|
|
71
|
+
# Sized rather than shrink-to-fit, so a one-word prompt and a three-line one
|
|
72
|
+
# come out the same. The floor is written as `min(20rem, 100%)` so it can
|
|
73
|
+
# never push the card wider than the space it has on a narrow screen.
|
|
74
|
+
CONFIRM_CONTENT_CLASSES = [
|
|
75
|
+
"relative w-full transform overflow-hidden rounded-lg text-left shadow-xl transition-all",
|
|
76
|
+
"min-w-[min(20rem,100%)] sm:max-w-md",
|
|
77
|
+
"bg-white dark:bg-gray-800 dark:text-white"
|
|
78
|
+
].join(" ")
|
|
79
|
+
|
|
80
|
+
CONFIRM_HEADER_CLASSES = [
|
|
81
|
+
"flex items-start justify-between gap-4 w-full px-6 pt-6",
|
|
82
|
+
"dark:border-gray-600",
|
|
83
|
+
"group-data-[header-divider=true]/utmr-modal:border-b group-data-[header-divider=true]/utmr-modal:pb-4",
|
|
84
|
+
"group-data-[header=false]/utmr-modal:hidden"
|
|
85
|
+
].join(" ")
|
|
86
|
+
|
|
87
|
+
CONFIRM_TITLE_CLASSES = "min-w-0"
|
|
88
|
+
CONFIRM_TITLE_H_CLASSES = "group-data-[title=false]/utmr-modal:hidden text-base font-semibold text-gray-900 dark:text-white"
|
|
89
|
+
|
|
90
|
+
CONFIRM_MAIN_CLASSES = "group-data-[padding=true]/utmr-modal:px-6 group-data-[padding=true]/utmr-modal:pt-2"
|
|
91
|
+
CONFIRM_BODY_CLASSES = "text-sm/6 text-gray-500 dark:text-gray-400"
|
|
92
|
+
|
|
93
|
+
CONFIRM_FOOTER_CLASSES = [
|
|
94
|
+
"px-6 pt-6 pb-6",
|
|
95
|
+
"dark:border-gray-600 group-data-[footer-divider=true]/utmr-modal:border-t"
|
|
96
|
+
].join(" ")
|
|
97
|
+
|
|
98
|
+
# Stacked and full-width on mobile (thumb-friendly), inline and right
|
|
99
|
+
# aligned from `sm` up. Reversed so the primary action sits on top.
|
|
100
|
+
CONFIRM_ACTIONS_CLASSES = "flex w-full flex-col-reverse gap-2 sm:flex-row sm:justify-end sm:gap-3"
|
|
101
|
+
|
|
102
|
+
CONFIRM_BUTTON_CLASSES = [
|
|
103
|
+
"inline-flex w-full sm:w-auto justify-center items-center rounded-md",
|
|
104
|
+
"px-4 py-2.5 sm:py-2 text-sm font-semibold transition-colors",
|
|
105
|
+
"focus-visible:outline-2 focus-visible:outline-offset-2"
|
|
106
|
+
].join(" ")
|
|
107
|
+
|
|
108
|
+
CONFIRM_CANCEL_CLASSES = [
|
|
109
|
+
CONFIRM_BUTTON_CLASSES,
|
|
110
|
+
"bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50",
|
|
111
|
+
"dark:bg-gray-700 dark:text-gray-100 dark:ring-gray-600 dark:hover:bg-gray-600",
|
|
112
|
+
"focus-visible:outline-gray-400"
|
|
113
|
+
].join(" ")
|
|
114
|
+
|
|
115
|
+
CONFIRM_ACCEPT_CLASSES = [
|
|
116
|
+
CONFIRM_BUTTON_CLASSES,
|
|
117
|
+
"text-white shadow-sm bg-indigo-600 hover:bg-indigo-500 focus-visible:outline-indigo-600",
|
|
118
|
+
# Destructive variant, set from `variant: :danger`
|
|
119
|
+
"group-data-[utmr-confirm-variant=danger]/utmr-modal:bg-red-600",
|
|
120
|
+
"group-data-[utmr-confirm-variant=danger]/utmr-modal:hover:bg-red-500",
|
|
121
|
+
"group-data-[utmr-confirm-variant=danger]/utmr-modal:focus-visible:outline-red-600"
|
|
122
|
+
].join(" ")
|
|
123
|
+
|
|
50
124
|
# Drawer constants
|
|
51
125
|
|
|
52
126
|
DRAWER_DIALOG_CLASSES = [
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# Set STYLES to a CSS string if you need additional inline styles.
|
|
6
6
|
module UltimateTurboModal::Flavors
|
|
7
7
|
class Vanilla < UltimateTurboModal::Base
|
|
8
|
-
STYLES = "html:has(dialog
|
|
8
|
+
STYLES = "html:has(dialog.utmr[open]) { overflow: hidden; }"
|
|
9
9
|
|
|
10
10
|
MODAL_DIALOG_CLASSES = "modal-container"
|
|
11
11
|
MODAL_INNER_CLASSES = "modal-inner"
|
|
@@ -20,6 +20,19 @@ module UltimateTurboModal::Flavors
|
|
|
20
20
|
MODAL_CLOSE_SR_CLASSES = "sr-only"
|
|
21
21
|
MODAL_CLOSE_ICON_CLASSES = "modal-close-icon"
|
|
22
22
|
|
|
23
|
+
# Confirm dialog constants
|
|
24
|
+
# Slots without a CONFIRM_* constant (dialog, inner) fall back to MODAL_*.
|
|
25
|
+
CONFIRM_CONTENT_CLASSES = "confirm-content"
|
|
26
|
+
CONFIRM_HEADER_CLASSES = "confirm-header"
|
|
27
|
+
CONFIRM_TITLE_CLASSES = "confirm-title"
|
|
28
|
+
CONFIRM_TITLE_H_CLASSES = "confirm-title-h"
|
|
29
|
+
CONFIRM_MAIN_CLASSES = "confirm-main"
|
|
30
|
+
CONFIRM_BODY_CLASSES = "confirm-body"
|
|
31
|
+
CONFIRM_FOOTER_CLASSES = "confirm-footer"
|
|
32
|
+
CONFIRM_ACTIONS_CLASSES = "confirm-actions"
|
|
33
|
+
CONFIRM_CANCEL_CLASSES = "confirm-cancel"
|
|
34
|
+
CONFIRM_ACCEPT_CLASSES = "confirm-accept"
|
|
35
|
+
|
|
23
36
|
# Drawer constants
|
|
24
37
|
DRAWER_DIALOG_CLASSES = "drawer-container"
|
|
25
38
|
DRAWER_WRAPPER_CLASSES = "drawer-wrapper"
|
|
@@ -27,4 +27,19 @@ UltimateTurboModal.configure do |config|
|
|
|
27
27
|
# d.overlay = true
|
|
28
28
|
# d.size = :md
|
|
29
29
|
# end
|
|
30
|
+
|
|
31
|
+
# Turbo Confirm support. Enable it by adding `<%= modal_confirm_template %>`
|
|
32
|
+
# to your layout; these are the dialog's defaults.
|
|
33
|
+
# config.confirm do |c|
|
|
34
|
+
# c.enabled = true
|
|
35
|
+
# c.title = "Are you sure?"
|
|
36
|
+
# c.accept_label = "OK"
|
|
37
|
+
# c.cancel_label = "Cancel"
|
|
38
|
+
# c.close_button = false
|
|
39
|
+
# c.header = true
|
|
40
|
+
# c.header_divider = false
|
|
41
|
+
# c.footer_divider = false
|
|
42
|
+
# c.padding = true
|
|
43
|
+
# c.overlay = true
|
|
44
|
+
# end
|
|
30
45
|
end
|
|
@@ -4,6 +4,7 @@ class UltimateTurboModal::Base < Phlex::HTML
|
|
|
4
4
|
prepend Phlex::DeferredRenderWithMainContent
|
|
5
5
|
|
|
6
6
|
attr_accessor :request, :allowed_click_outside_selector, :content_div_data
|
|
7
|
+
CONFIRM_TEMPLATE_ID = "utmr-confirm-template"
|
|
7
8
|
VALID_DRAWER_SIZES = %i[xs sm md lg xl 2xl full].freeze
|
|
8
9
|
VALID_DRAWER_POSITIONS = %i[right left].freeze
|
|
9
10
|
|
|
@@ -13,6 +14,7 @@ class UltimateTurboModal::Base < Phlex::HTML
|
|
|
13
14
|
# @param close_button_data_action [String] `data-action` attribute for the close button
|
|
14
15
|
# @param close_button_sr_label [String] Close button label for screen readers
|
|
15
16
|
# @param close_on_submit [Boolean] Whether to dismiss on a successful, non-redirecting form submission
|
|
17
|
+
# @param confirm [Boolean] Internal: render the Turbo Confirm dialog template. Use the `modal_confirm_template` view helper instead.
|
|
16
18
|
# @param drawer_position [Symbol, false] Internal: drawer position (:right, :left) or false for standard modal. Use the `drawer()` view helper instead.
|
|
17
19
|
# @param footer_divider [Boolean] Whether to show a divider between the main content and the footer
|
|
18
20
|
# @param header [Boolean] Whether to show a modal header
|
|
@@ -30,6 +32,7 @@ class UltimateTurboModal::Base < Phlex::HTML
|
|
|
30
32
|
close_button_data_action: "modal#hideModal",
|
|
31
33
|
close_button_sr_label: "Close modal",
|
|
32
34
|
close_on_submit: nil,
|
|
35
|
+
confirm: false,
|
|
33
36
|
drawer_position: false,
|
|
34
37
|
footer_divider: nil,
|
|
35
38
|
header: nil,
|
|
@@ -38,14 +41,23 @@ class UltimateTurboModal::Base < Phlex::HTML
|
|
|
38
41
|
padding: nil,
|
|
39
42
|
size: nil,
|
|
40
43
|
content_div_data: nil,
|
|
44
|
+
accept_label: nil,
|
|
45
|
+
cancel_label: nil,
|
|
41
46
|
request: nil, title: nil
|
|
42
47
|
)
|
|
43
48
|
@drawer = drawer_position
|
|
49
|
+
@confirm = confirm
|
|
44
50
|
@request = request
|
|
45
51
|
|
|
46
52
|
raise ArgumentError, "Cannot render a drawer into the drawer-modal frame (drawers cannot be opened from inside another drawer or modal)" if drawer? && stacked?
|
|
47
53
|
|
|
48
|
-
if
|
|
54
|
+
if confirm?
|
|
55
|
+
cfg = UltimateTurboModal.configuration.confirm_config
|
|
56
|
+
@drawer_size = nil
|
|
57
|
+
@accept_label = accept_label || cfg.accept_label
|
|
58
|
+
@cancel_label = cancel_label || cfg.cancel_label
|
|
59
|
+
title ||= cfg.title
|
|
60
|
+
elsif drawer?
|
|
49
61
|
cfg = UltimateTurboModal.configuration.drawer_config
|
|
50
62
|
@drawer_size = self.class.validate_drawer_size!(size || cfg.size)
|
|
51
63
|
else
|
|
@@ -90,7 +102,11 @@ class UltimateTurboModal::Base < Phlex::HTML
|
|
|
90
102
|
end
|
|
91
103
|
|
|
92
104
|
def view_template(&block)
|
|
93
|
-
if
|
|
105
|
+
if confirm?
|
|
106
|
+
# Inert until the JS clones it, so it can live in the layout on every
|
|
107
|
+
# page without a dialog (or its <style> tag) ever being active.
|
|
108
|
+
template(id: CONFIRM_TEMPLATE_ID) { render_confirm }
|
|
109
|
+
elsif turbo_frame?
|
|
94
110
|
turbo_frame_tag(turbo_frame_name) do
|
|
95
111
|
drawer? ? render_drawer(&block) : render_modal(&block)
|
|
96
112
|
end
|
|
@@ -103,12 +119,16 @@ class UltimateTurboModal::Base < Phlex::HTML
|
|
|
103
119
|
stacked? ? "drawer-modal" : "modal"
|
|
104
120
|
end
|
|
105
121
|
|
|
122
|
+
# Both return nil so `<%= m.title do %>` writes nothing to the ERB buffer.
|
|
123
|
+
# The assignment's own value is a Proc, and ERB would print it.
|
|
106
124
|
def title(&block)
|
|
107
125
|
@title_block = block
|
|
126
|
+
nil
|
|
108
127
|
end
|
|
109
128
|
|
|
110
129
|
def footer(&block)
|
|
111
130
|
@footer = block
|
|
131
|
+
nil
|
|
112
132
|
end
|
|
113
133
|
|
|
114
134
|
class << self
|
|
@@ -136,13 +156,16 @@ class UltimateTurboModal::Base < Phlex::HTML
|
|
|
136
156
|
|
|
137
157
|
def close_on_submit? = !!@close_on_submit
|
|
138
158
|
|
|
159
|
+
def confirm? = !!@confirm
|
|
160
|
+
|
|
139
161
|
def title_block? = !!@title_block
|
|
140
162
|
|
|
141
163
|
def title? = !!@title
|
|
142
164
|
|
|
143
165
|
def header? = !!@header
|
|
144
166
|
|
|
145
|
-
|
|
167
|
+
# A confirm's footer holds its own buttons rather than a user block.
|
|
168
|
+
def footer? = @footer.present? || confirm?
|
|
146
169
|
|
|
147
170
|
def header_divider? = !!@header_divider && (@title_block.present? || title?)
|
|
148
171
|
|
|
@@ -168,13 +191,22 @@ class UltimateTurboModal::Base < Phlex::HTML
|
|
|
168
191
|
frame == "drawer-modal" || frame == "modal-inner-stacked"
|
|
169
192
|
end
|
|
170
193
|
|
|
171
|
-
def dialog_id =
|
|
194
|
+
def dialog_id = scoped_id("modal-container")
|
|
172
195
|
|
|
173
|
-
def inner_id =
|
|
196
|
+
def inner_id = scoped_id("modal-inner")
|
|
174
197
|
|
|
175
198
|
# Suffix inner ids so they don't collide with the drawer's ids when the
|
|
176
|
-
# stacked modal is rendered inside the drawer's DOM
|
|
177
|
-
|
|
199
|
+
# stacked modal is rendered inside the drawer's DOM, or with an open
|
|
200
|
+
# modal's ids when the confirm dialog is layered over it.
|
|
201
|
+
def scoped_id(name) = "#{name}#{id_suffix}"
|
|
202
|
+
|
|
203
|
+
def id_suffix
|
|
204
|
+
return "-confirm" if confirm?
|
|
205
|
+
# A layout renders for Turbo Frame requests too, so this has to be checked
|
|
206
|
+
# after confirm? -- otherwise a confirm template rendered during a stacked
|
|
207
|
+
# request would claim the stacked modal's ids.
|
|
208
|
+
stacked? ? "-stacked" : ""
|
|
209
|
+
end
|
|
178
210
|
|
|
179
211
|
def drawer_position = @drawer || :right
|
|
180
212
|
|
|
@@ -252,11 +284,42 @@ class UltimateTurboModal::Base < Phlex::HTML
|
|
|
252
284
|
raw(safe(str))
|
|
253
285
|
end
|
|
254
286
|
|
|
287
|
+
# Confirm dialogs render through the modal's markup, so every slot falls back
|
|
288
|
+
# to its MODAL_* class unless the flavor defines a CONFIRM_* override.
|
|
255
289
|
def classes_for(suffix)
|
|
290
|
+
if confirm?
|
|
291
|
+
override = confirm_classes_for(suffix)
|
|
292
|
+
return override unless override.nil?
|
|
293
|
+
end
|
|
294
|
+
|
|
256
295
|
prefix = drawer? ? "DRAWER" : "MODAL"
|
|
257
296
|
self.class.const_get("#{prefix}_#{suffix}")
|
|
258
297
|
end
|
|
259
298
|
|
|
299
|
+
# CONFIRM_* constants are optional. A flavor file written before confirm
|
|
300
|
+
# support returns nil here, which means shared slots fall back to MODAL_* and
|
|
301
|
+
# the confirm-only slots (body, actions, buttons) render unstyled rather than
|
|
302
|
+
# raising. `warn_missing_confirm_classes` is what tells the developer why.
|
|
303
|
+
def confirm_classes_for(suffix)
|
|
304
|
+
const = :"CONFIRM_#{suffix}"
|
|
305
|
+
self.class.const_defined?(const) ? self.class.const_get(const) : nil
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# An unstyled confirm dialog looks like a bug in the gem rather than a stale
|
|
309
|
+
# flavor file, so say which it is. Once per flavor class, in development only.
|
|
310
|
+
def warn_missing_confirm_classes
|
|
311
|
+
return unless defined?(Rails) && Rails.env.local?
|
|
312
|
+
return if self.class.const_defined?(:CONFIRM_ACTIONS_CLASSES)
|
|
313
|
+
return if self.class.instance_variable_get(:@utmr_confirm_classes_warned)
|
|
314
|
+
|
|
315
|
+
self.class.instance_variable_set(:@utmr_confirm_classes_warned, true)
|
|
316
|
+
Rails.logger&.warn(
|
|
317
|
+
"[UltimateTurboModal] #{self.class.name} defines no CONFIRM_* classes, so the " \
|
|
318
|
+
"confirm dialog will render unstyled. Run `rails g ultimate_turbo_modal:update` " \
|
|
319
|
+
"to refresh your flavor file."
|
|
320
|
+
)
|
|
321
|
+
end
|
|
322
|
+
|
|
260
323
|
def custom_drawer_size?
|
|
261
324
|
@drawer_size.present? && !VALID_DRAWER_SIZES.include?(@drawer_size.to_s.to_sym)
|
|
262
325
|
end
|
|
@@ -265,23 +328,35 @@ class UltimateTurboModal::Base < Phlex::HTML
|
|
|
265
328
|
data_attributes = {
|
|
266
329
|
controller: "modal",
|
|
267
330
|
modal_target: "container",
|
|
268
|
-
|
|
269
|
-
modal_allowed_click_outside_selector_value: allowed_click_outside_selector,
|
|
270
|
-
modal_close_on_submit_value: close_on_submit?.to_s,
|
|
271
|
-
action: "turbo:submit-end->modal#submitEnd cancel->modal#cancelEvent mousedown->modal#dialogMousedown click->modal#dialogClicked",
|
|
331
|
+
action: dialog_actions,
|
|
272
332
|
padding: padding?.to_s,
|
|
273
333
|
title: title?.to_s,
|
|
274
334
|
header: header?.to_s,
|
|
275
335
|
close_button: close_button?.to_s,
|
|
276
336
|
header_divider: header_divider?.to_s,
|
|
277
|
-
footer_divider: footer_divider?.to_s
|
|
337
|
+
footer_divider: footer_divider?.to_s,
|
|
338
|
+
overlay: overlay?.to_s
|
|
278
339
|
}
|
|
279
340
|
|
|
341
|
+
role = nil
|
|
342
|
+
aria_attributes = {labelledby: scoped_id("modal-title-h")}
|
|
343
|
+
|
|
344
|
+
if confirm?
|
|
345
|
+
# Everything the controller reads from those three values is driven by an
|
|
346
|
+
# action a confirm doesn't bind, so they would only ever mislead a reader.
|
|
347
|
+
data_attributes[:utmr_confirm] = ""
|
|
348
|
+
role = "alertdialog"
|
|
349
|
+
aria_attributes[:describedby] = scoped_id("modal-body")
|
|
350
|
+
else
|
|
351
|
+
data_attributes[:modal_advance_url_value] = advance_url
|
|
352
|
+
data_attributes[:modal_allowed_click_outside_selector_value] = allowed_click_outside_selector
|
|
353
|
+
data_attributes[:modal_close_on_submit_value] = close_on_submit?.to_s
|
|
354
|
+
end
|
|
355
|
+
|
|
280
356
|
if drawer?
|
|
281
357
|
data_attributes[:drawer] = drawer_position.to_s
|
|
282
358
|
data_attributes[:drawer_size] = (@drawer_size.presence || "md").to_s
|
|
283
359
|
end
|
|
284
|
-
data_attributes[:overlay] = overlay?.to_s
|
|
285
360
|
|
|
286
361
|
if defined?(Rails) && Rails.env.local?
|
|
287
362
|
data_attributes[:utmr_version] = UltimateTurboModal::VERSION
|
|
@@ -297,17 +372,29 @@ class UltimateTurboModal::Base < Phlex::HTML
|
|
|
297
372
|
dialog(id: dialog_id,
|
|
298
373
|
class: dialog_classes,
|
|
299
374
|
style: inline_style,
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
},
|
|
375
|
+
role: role,
|
|
376
|
+
aria: aria_attributes,
|
|
303
377
|
data: data_attributes, &block)
|
|
304
378
|
end
|
|
305
379
|
|
|
380
|
+
# A confirm is a decision, so a stray click on the backdrop must not answer it
|
|
381
|
+
# for the user. Leaving the outside-click actions off is the whole of it --
|
|
382
|
+
# Escape and the Cancel button still dismiss.
|
|
383
|
+
def dialog_actions
|
|
384
|
+
return "cancel->modal#cancelEvent" if confirm?
|
|
385
|
+
|
|
386
|
+
["turbo:submit-end->modal#submitEnd",
|
|
387
|
+
"cancel->modal#cancelEvent",
|
|
388
|
+
"mousedown->modal#dialogMousedown",
|
|
389
|
+
"click->modal#dialogClicked"].join(" ")
|
|
390
|
+
end
|
|
391
|
+
|
|
306
392
|
## Modal-specific elements
|
|
307
393
|
|
|
308
394
|
def modal_inner(&block)
|
|
309
395
|
maybe_turbo_frame(inner_id) do
|
|
310
|
-
div(id: inner_id, class: self.class::MODAL_INNER_CLASSES,
|
|
396
|
+
div(id: inner_id, class: self.class::MODAL_INNER_CLASSES,
|
|
397
|
+
data: {modal_target: "transition"}, &block)
|
|
311
398
|
end
|
|
312
399
|
end
|
|
313
400
|
|
|
@@ -336,7 +423,8 @@ class UltimateTurboModal::Base < Phlex::HTML
|
|
|
336
423
|
end
|
|
337
424
|
|
|
338
425
|
def drawer_panel(&block)
|
|
339
|
-
div(id: "drawer-panel", class: self.class::DRAWER_PANEL_CLASSES,
|
|
426
|
+
div(id: "drawer-panel", class: self.class::DRAWER_PANEL_CLASSES,
|
|
427
|
+
data: {modal_target: "content transition"}, &block)
|
|
340
428
|
end
|
|
341
429
|
|
|
342
430
|
def drawer_content(&block)
|
|
@@ -354,6 +442,51 @@ class UltimateTurboModal::Base < Phlex::HTML
|
|
|
354
442
|
end
|
|
355
443
|
end
|
|
356
444
|
|
|
445
|
+
## Confirm-specific elements
|
|
446
|
+
|
|
447
|
+
def render_confirm
|
|
448
|
+
warn_missing_confirm_classes
|
|
449
|
+
# Unlike the modal and drawer templates, the styles go *inside* the dialog:
|
|
450
|
+
# the JS clones this one node out of the <template>, and the scroll-lock
|
|
451
|
+
# rule has to come along with it and leave with it.
|
|
452
|
+
dialog_element do
|
|
453
|
+
styles
|
|
454
|
+
confirm_inner do
|
|
455
|
+
confirm_content do
|
|
456
|
+
render_header
|
|
457
|
+
confirm_main
|
|
458
|
+
confirm_footer
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def confirm_inner(&block)
|
|
465
|
+
div(id: inner_id, class: classes_for("INNER_CLASSES"), data: {modal_target: "transition"}, &block)
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def confirm_content(&block)
|
|
469
|
+
div(id: scoped_id("modal-content"), class: classes_for("CONTENT_CLASSES"), data: {modal_target: "content"}, &block)
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
# Left empty: the JavaScript fills it from `data-turbo-confirm`.
|
|
473
|
+
def confirm_main
|
|
474
|
+
render_main do
|
|
475
|
+
div(id: scoped_id("modal-body"), class: confirm_classes_for("BODY_CLASSES"))
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def confirm_footer
|
|
480
|
+
div(id: scoped_id("modal-footer"), class: classes_for("FOOTER_CLASSES")) do
|
|
481
|
+
div(class: confirm_classes_for("ACTIONS_CLASSES")) do
|
|
482
|
+
button(type: "button", class: confirm_classes_for("CANCEL_CLASSES"),
|
|
483
|
+
data: {utmr_confirm_action: "cancel"}) { @cancel_label }
|
|
484
|
+
button(type: "button", class: confirm_classes_for("ACCEPT_CLASSES"),
|
|
485
|
+
data: {utmr_confirm_action: "accept"}) { @accept_label }
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
end
|
|
489
|
+
|
|
357
490
|
## Shared rendering
|
|
358
491
|
|
|
359
492
|
def render_main(&block)
|
|
@@ -363,10 +496,20 @@ class UltimateTurboModal::Base < Phlex::HTML
|
|
|
363
496
|
def render_header
|
|
364
497
|
div(id: scoped_id("modal-header"), class: classes_for("HEADER_CLASSES")) do
|
|
365
498
|
render_title
|
|
366
|
-
|
|
499
|
+
render_header_close
|
|
367
500
|
end
|
|
368
501
|
end
|
|
369
502
|
|
|
503
|
+
# Modals and drawers always render the close button and let the flavor hide it
|
|
504
|
+
# through `data-close-button`. A confirm leaves it out of the markup entirely
|
|
505
|
+
# instead, which keeps it out of the tab order and the accessibility tree --
|
|
506
|
+
# it has its own Cancel button, so it is off by default.
|
|
507
|
+
def render_header_close
|
|
508
|
+
return if confirm? && !close_button?
|
|
509
|
+
|
|
510
|
+
drawer? ? drawer_close : modal_close
|
|
511
|
+
end
|
|
512
|
+
|
|
370
513
|
def render_title
|
|
371
514
|
div(id: scoped_id("modal-title"), class: classes_for("TITLE_CLASSES")) do
|
|
372
515
|
if @title_block.present?
|
|
@@ -14,7 +14,7 @@ module UltimateTurboModal
|
|
|
14
14
|
:allowed_click_outside_selector, :allowed_click_outside_selector=, to: :configuration
|
|
15
15
|
|
|
16
16
|
class Configuration
|
|
17
|
-
attr_reader :flavor, :modal_config, :drawer_config
|
|
17
|
+
attr_reader :flavor, :modal_config, :drawer_config, :confirm_config
|
|
18
18
|
attr_accessor :allowed_click_outside_selector
|
|
19
19
|
|
|
20
20
|
def initialize
|
|
@@ -22,6 +22,7 @@ module UltimateTurboModal
|
|
|
22
22
|
@allowed_click_outside_selector = []
|
|
23
23
|
@modal_config = ModalConfig.new
|
|
24
24
|
@drawer_config = DrawerConfig.new
|
|
25
|
+
@confirm_config = ConfirmConfig.new
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
def modal
|
|
@@ -34,6 +35,11 @@ module UltimateTurboModal
|
|
|
34
35
|
@drawer_config
|
|
35
36
|
end
|
|
36
37
|
|
|
38
|
+
def confirm
|
|
39
|
+
yield(@confirm_config) if block_given?
|
|
40
|
+
@confirm_config
|
|
41
|
+
end
|
|
42
|
+
|
|
37
43
|
def flavor=(flavor)
|
|
38
44
|
raise ArgumentError, "Value must be a symbol." unless flavor.is_a?(Symbol) || flavor.is_a?(String)
|
|
39
45
|
@flavor = flavor.to_sym
|
|
@@ -87,6 +93,32 @@ module UltimateTurboModal
|
|
|
87
93
|
end
|
|
88
94
|
end
|
|
89
95
|
|
|
96
|
+
# Defaults for the Turbo Confirm dialog. Every value here is a fallback:
|
|
97
|
+
# anything supplied per-element through `data-turbo-confirm` wins.
|
|
98
|
+
class ConfirmConfig < BaseConfig
|
|
99
|
+
attr_accessor :title, :accept_label, :cancel_label
|
|
100
|
+
attr_reader :enabled
|
|
101
|
+
|
|
102
|
+
boolean_option :enabled
|
|
103
|
+
|
|
104
|
+
# `advance` and `close_on_submit` are left unset: a confirm never pushes a
|
|
105
|
+
# URL and holds no form, and the dialog binds neither behavior.
|
|
106
|
+
def initialize
|
|
107
|
+
@enabled = true
|
|
108
|
+
# A confirm has its own Cancel button, and dividers chop up what is
|
|
109
|
+
# only ever a line or two of text.
|
|
110
|
+
@close_button = false
|
|
111
|
+
@header = true
|
|
112
|
+
@header_divider = false
|
|
113
|
+
@footer_divider = false
|
|
114
|
+
@padding = true
|
|
115
|
+
@overlay = true
|
|
116
|
+
@title = "Are you sure?"
|
|
117
|
+
@accept_label = "OK"
|
|
118
|
+
@cancel_label = "Cancel"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
90
122
|
class DrawerConfig < BaseConfig
|
|
91
123
|
attr_reader :size, :position
|
|
92
124
|
|
|
@@ -12,5 +12,36 @@ module UltimateTurboModal::Helpers
|
|
|
12
12
|
size = UltimateTurboModal::Base.validate_drawer_size!(size || cfg.size)
|
|
13
13
|
modal(drawer_position: position, size: size, **options, &block)
|
|
14
14
|
end
|
|
15
|
+
|
|
16
|
+
# Renders the inert <template> the Turbo Confirm dialog is cloned from.
|
|
17
|
+
# Place it once in your layout: its presence is what enables the feature.
|
|
18
|
+
# Renders nothing when `config.confirm.enabled` is false, which turns the
|
|
19
|
+
# feature off without touching the layout.
|
|
20
|
+
def modal_confirm_template(**options)
|
|
21
|
+
return unless UltimateTurboModal.configuration.confirm_config.enabled
|
|
22
|
+
|
|
23
|
+
render(UltimateTurboModal.confirm(request:, **options))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Builds the `data` attributes for an element that should prompt before
|
|
27
|
+
# acting. The options are JSON-encoded into `data-turbo-confirm` because
|
|
28
|
+
# Turbo rewrites a link with `data-turbo-method` into a synthetic form and
|
|
29
|
+
# copies only a fixed set of attributes across -- sibling
|
|
30
|
+
# `data-turbo-confirm-*` attributes on a link would be dropped.
|
|
31
|
+
#
|
|
32
|
+
# link_to "Delete", post_path(post), data: modal_confirm(
|
|
33
|
+
# "This can't be undone.", title: "Delete post?", accept: "Delete",
|
|
34
|
+
# variant: :danger, turbo_method: :delete)
|
|
35
|
+
#
|
|
36
|
+
# `native: true` opts this one confirmation back out to `window.confirm`.
|
|
37
|
+
# On a link that is the only way to say it, since Turbo drops sibling
|
|
38
|
+
# `data-turbo-confirm-native` along with everything else.
|
|
39
|
+
def modal_confirm(body = nil, title: nil, accept: nil, cancel: nil, variant: nil, native: nil, **data)
|
|
40
|
+
payload = {title: title, body: body, accept: accept, cancel: cancel,
|
|
41
|
+
variant: variant, native: native&.to_s}.compact
|
|
42
|
+
raise ArgumentError, "modal_confirm requires a body or at least one option" if payload.empty?
|
|
43
|
+
|
|
44
|
+
{turbo_confirm: payload.to_json}.merge(data)
|
|
45
|
+
end
|
|
15
46
|
end
|
|
16
47
|
end
|
data/lib/ultimate_turbo_modal.rb
CHANGED