ultimate_turbo_modal 3.2.1 → 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 +13 -0
- data/Gemfile.lock +1 -1
- data/README.md +248 -0
- data/VERSION +1 -1
- data/app/assets/stylesheets/ultimate_turbo_modal.css +610 -0
- data/lib/generators/ultimate_turbo_modal/base.rb +44 -0
- data/lib/generators/ultimate_turbo_modal/install_generator.rb +8 -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 +18 -0
- data/lib/generators/ultimate_turbo_modal/update_generator.rb +2 -0
- data/lib/ultimate_turbo_modal/base.rb +172 -26
- data/lib/ultimate_turbo_modal/configuration.rb +45 -5
- data/lib/ultimate_turbo_modal/helpers/view_helper.rb +31 -0
- data/lib/ultimate_turbo_modal/railtie.rb +23 -0
- data/lib/ultimate_turbo_modal.rb +4 -0
- metadata +2 -1
|
@@ -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"
|
|
@@ -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
|
|
@@ -24,4 +27,19 @@ UltimateTurboModal.configure do |config|
|
|
|
24
27
|
# d.overlay = true
|
|
25
28
|
# d.size = :md
|
|
26
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
|
|
27
45
|
end
|
|
@@ -4,14 +4,17 @@ 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
|
|
|
10
|
-
# @param advance [Boolean, String] Whether to update the browser history when opening and closing the modal
|
|
11
|
+
# @param advance [Boolean, String] Whether to update the browser history when opening and closing the modal or drawer
|
|
11
12
|
# @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
13
|
# @param close_button [Boolean] Whether to show a close button
|
|
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
|
|
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.
|
|
15
18
|
# @param drawer_position [Symbol, false] Internal: drawer position (:right, :left) or false for standard modal. Use the `drawer()` view helper instead.
|
|
16
19
|
# @param footer_divider [Boolean] Whether to show a divider between the main content and the footer
|
|
17
20
|
# @param header [Boolean] Whether to show a modal header
|
|
@@ -28,6 +31,8 @@ class UltimateTurboModal::Base < Phlex::HTML
|
|
|
28
31
|
close_button: nil,
|
|
29
32
|
close_button_data_action: "modal#hideModal",
|
|
30
33
|
close_button_sr_label: "Close modal",
|
|
34
|
+
close_on_submit: nil,
|
|
35
|
+
confirm: false,
|
|
31
36
|
drawer_position: false,
|
|
32
37
|
footer_divider: nil,
|
|
33
38
|
header: nil,
|
|
@@ -36,27 +41,34 @@ class UltimateTurboModal::Base < Phlex::HTML
|
|
|
36
41
|
padding: nil,
|
|
37
42
|
size: nil,
|
|
38
43
|
content_div_data: nil,
|
|
44
|
+
accept_label: nil,
|
|
45
|
+
cancel_label: nil,
|
|
39
46
|
request: nil, title: nil
|
|
40
47
|
)
|
|
41
48
|
@drawer = drawer_position
|
|
49
|
+
@confirm = confirm
|
|
42
50
|
@request = request
|
|
43
51
|
|
|
44
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?
|
|
45
53
|
|
|
46
|
-
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?
|
|
47
61
|
cfg = UltimateTurboModal.configuration.drawer_config
|
|
48
|
-
@advance = false
|
|
49
|
-
@advance_url = nil
|
|
50
|
-
@close_button = close_button.nil? ? cfg.close_button : close_button
|
|
51
62
|
@drawer_size = self.class.validate_drawer_size!(size || cfg.size)
|
|
52
63
|
else
|
|
53
64
|
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
65
|
@drawer_size = nil
|
|
59
66
|
end
|
|
67
|
+
adv = advance.nil? ? cfg.advance : advance
|
|
68
|
+
@advance = !!adv
|
|
69
|
+
@advance_url = (adv.present? && adv.is_a?(String)) ? adv : nil
|
|
70
|
+
@close_button = close_button.nil? ? cfg.close_button : close_button
|
|
71
|
+
@close_on_submit = close_on_submit.nil? ? cfg.close_on_submit : close_on_submit
|
|
60
72
|
@footer_divider = footer_divider.nil? ? cfg.footer_divider : footer_divider
|
|
61
73
|
@header = header.nil? ? cfg.header : header
|
|
62
74
|
@header_divider = header_divider.nil? ? cfg.header_divider : header_divider
|
|
@@ -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
|
|
@@ -134,13 +154,18 @@ class UltimateTurboModal::Base < Phlex::HTML
|
|
|
134
154
|
|
|
135
155
|
def close_button? = !!@close_button
|
|
136
156
|
|
|
157
|
+
def close_on_submit? = !!@close_on_submit
|
|
158
|
+
|
|
159
|
+
def confirm? = !!@confirm
|
|
160
|
+
|
|
137
161
|
def title_block? = !!@title_block
|
|
138
162
|
|
|
139
163
|
def title? = !!@title
|
|
140
164
|
|
|
141
165
|
def header? = !!@header
|
|
142
166
|
|
|
143
|
-
|
|
167
|
+
# A confirm's footer holds its own buttons rather than a user block.
|
|
168
|
+
def footer? = @footer.present? || confirm?
|
|
144
169
|
|
|
145
170
|
def header_divider? = !!@header_divider && (@title_block.present? || title?)
|
|
146
171
|
|
|
@@ -166,13 +191,22 @@ class UltimateTurboModal::Base < Phlex::HTML
|
|
|
166
191
|
frame == "drawer-modal" || frame == "modal-inner-stacked"
|
|
167
192
|
end
|
|
168
193
|
|
|
169
|
-
def dialog_id =
|
|
194
|
+
def dialog_id = scoped_id("modal-container")
|
|
170
195
|
|
|
171
|
-
def inner_id =
|
|
196
|
+
def inner_id = scoped_id("modal-inner")
|
|
172
197
|
|
|
173
198
|
# Suffix inner ids so they don't collide with the drawer's ids when the
|
|
174
|
-
# stacked modal is rendered inside the drawer's DOM
|
|
175
|
-
|
|
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
|
|
176
210
|
|
|
177
211
|
def drawer_position = @drawer || :right
|
|
178
212
|
|
|
@@ -250,11 +284,42 @@ class UltimateTurboModal::Base < Phlex::HTML
|
|
|
250
284
|
raw(safe(str))
|
|
251
285
|
end
|
|
252
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.
|
|
253
289
|
def classes_for(suffix)
|
|
290
|
+
if confirm?
|
|
291
|
+
override = confirm_classes_for(suffix)
|
|
292
|
+
return override unless override.nil?
|
|
293
|
+
end
|
|
294
|
+
|
|
254
295
|
prefix = drawer? ? "DRAWER" : "MODAL"
|
|
255
296
|
self.class.const_get("#{prefix}_#{suffix}")
|
|
256
297
|
end
|
|
257
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
|
+
|
|
258
323
|
def custom_drawer_size?
|
|
259
324
|
@drawer_size.present? && !VALID_DRAWER_SIZES.include?(@drawer_size.to_s.to_sym)
|
|
260
325
|
end
|
|
@@ -263,22 +328,35 @@ class UltimateTurboModal::Base < Phlex::HTML
|
|
|
263
328
|
data_attributes = {
|
|
264
329
|
controller: "modal",
|
|
265
330
|
modal_target: "container",
|
|
266
|
-
|
|
267
|
-
modal_allowed_click_outside_selector_value: allowed_click_outside_selector,
|
|
268
|
-
action: "turbo:submit-end->modal#submitEnd cancel->modal#cancelEvent mousedown->modal#dialogMousedown click->modal#dialogClicked",
|
|
331
|
+
action: dialog_actions,
|
|
269
332
|
padding: padding?.to_s,
|
|
270
333
|
title: title?.to_s,
|
|
271
334
|
header: header?.to_s,
|
|
272
335
|
close_button: close_button?.to_s,
|
|
273
336
|
header_divider: header_divider?.to_s,
|
|
274
|
-
footer_divider: footer_divider?.to_s
|
|
337
|
+
footer_divider: footer_divider?.to_s,
|
|
338
|
+
overlay: overlay?.to_s
|
|
275
339
|
}
|
|
276
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
|
+
|
|
277
356
|
if drawer?
|
|
278
357
|
data_attributes[:drawer] = drawer_position.to_s
|
|
279
358
|
data_attributes[:drawer_size] = (@drawer_size.presence || "md").to_s
|
|
280
359
|
end
|
|
281
|
-
data_attributes[:overlay] = overlay?.to_s
|
|
282
360
|
|
|
283
361
|
if defined?(Rails) && Rails.env.local?
|
|
284
362
|
data_attributes[:utmr_version] = UltimateTurboModal::VERSION
|
|
@@ -294,17 +372,29 @@ class UltimateTurboModal::Base < Phlex::HTML
|
|
|
294
372
|
dialog(id: dialog_id,
|
|
295
373
|
class: dialog_classes,
|
|
296
374
|
style: inline_style,
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
},
|
|
375
|
+
role: role,
|
|
376
|
+
aria: aria_attributes,
|
|
300
377
|
data: data_attributes, &block)
|
|
301
378
|
end
|
|
302
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
|
+
|
|
303
392
|
## Modal-specific elements
|
|
304
393
|
|
|
305
394
|
def modal_inner(&block)
|
|
306
395
|
maybe_turbo_frame(inner_id) do
|
|
307
|
-
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)
|
|
308
398
|
end
|
|
309
399
|
end
|
|
310
400
|
|
|
@@ -333,7 +423,8 @@ class UltimateTurboModal::Base < Phlex::HTML
|
|
|
333
423
|
end
|
|
334
424
|
|
|
335
425
|
def drawer_panel(&block)
|
|
336
|
-
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)
|
|
337
428
|
end
|
|
338
429
|
|
|
339
430
|
def drawer_content(&block)
|
|
@@ -351,6 +442,51 @@ class UltimateTurboModal::Base < Phlex::HTML
|
|
|
351
442
|
end
|
|
352
443
|
end
|
|
353
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
|
+
|
|
354
490
|
## Shared rendering
|
|
355
491
|
|
|
356
492
|
def render_main(&block)
|
|
@@ -360,10 +496,20 @@ class UltimateTurboModal::Base < Phlex::HTML
|
|
|
360
496
|
def render_header
|
|
361
497
|
div(id: scoped_id("modal-header"), class: classes_for("HEADER_CLASSES")) do
|
|
362
498
|
render_title
|
|
363
|
-
|
|
499
|
+
render_header_close
|
|
364
500
|
end
|
|
365
501
|
end
|
|
366
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
|
+
|
|
367
513
|
def render_title
|
|
368
514
|
div(id: scoped_id("modal-title"), class: classes_for("TITLE_CLASSES")) do
|
|
369
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
|
|
@@ -41,7 +47,7 @@ module UltimateTurboModal
|
|
|
41
47
|
|
|
42
48
|
# Shared base for modal and drawer configuration
|
|
43
49
|
class BaseConfig
|
|
44
|
-
attr_reader :close_button, :header, :header_divider, :footer_divider, :padding, :overlay
|
|
50
|
+
attr_reader :advance, :close_button, :close_on_submit, :header, :header_divider, :footer_divider, :padding, :overlay
|
|
45
51
|
|
|
46
52
|
def self.boolean_option(name)
|
|
47
53
|
define_method(:"#{name}=") do |value|
|
|
@@ -51,11 +57,20 @@ module UltimateTurboModal
|
|
|
51
57
|
end
|
|
52
58
|
|
|
53
59
|
boolean_option :close_button
|
|
60
|
+
boolean_option :close_on_submit
|
|
54
61
|
boolean_option :header
|
|
55
62
|
boolean_option :header_divider
|
|
56
63
|
boolean_option :footer_divider
|
|
57
64
|
boolean_option :overlay
|
|
58
65
|
|
|
66
|
+
def advance=(value)
|
|
67
|
+
if [true, false].include?(value) || value.is_a?(String)
|
|
68
|
+
@advance = value
|
|
69
|
+
else
|
|
70
|
+
raise ArgumentError, "Value must be a boolean or a String."
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
59
74
|
def padding=(padding)
|
|
60
75
|
if [true, false].include?(padding) || padding.is_a?(String)
|
|
61
76
|
@padding = padding
|
|
@@ -66,26 +81,51 @@ module UltimateTurboModal
|
|
|
66
81
|
end
|
|
67
82
|
|
|
68
83
|
class ModalConfig < BaseConfig
|
|
69
|
-
attr_reader :advance
|
|
70
|
-
|
|
71
84
|
def initialize
|
|
72
85
|
@advance = false
|
|
73
86
|
@close_button = true
|
|
87
|
+
@close_on_submit = true
|
|
74
88
|
@header = true
|
|
75
89
|
@header_divider = true
|
|
76
90
|
@footer_divider = true
|
|
77
91
|
@padding = true
|
|
78
92
|
@overlay = true
|
|
79
93
|
end
|
|
94
|
+
end
|
|
80
95
|
|
|
81
|
-
|
|
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
|
|
82
120
|
end
|
|
83
121
|
|
|
84
122
|
class DrawerConfig < BaseConfig
|
|
85
123
|
attr_reader :size, :position
|
|
86
124
|
|
|
87
125
|
def initialize
|
|
126
|
+
@advance = false
|
|
88
127
|
@close_button = true
|
|
128
|
+
@close_on_submit = true
|
|
89
129
|
@header = true
|
|
90
130
|
@header_divider = false
|
|
91
131
|
@footer_divider = true
|
|
@@ -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
|