modal_stack 0.2.0 → 0.4.1
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 +59 -0
- data/README.md +136 -52
- data/app/assets/javascripts/modal_stack.js +612 -63
- data/app/assets/stylesheets/modal_stack/bootstrap.css +120 -11
- data/app/assets/stylesheets/modal_stack/{tailwind.css → tailwind_v3.css} +82 -14
- data/app/assets/stylesheets/modal_stack/tailwind_v4.css +372 -0
- data/app/assets/stylesheets/modal_stack/vanilla.css +128 -11
- data/app/javascript/modal_stack/controllers/modal_stack_back_link_controller.js +32 -0
- data/app/javascript/modal_stack/controllers/modal_stack_controller.js +54 -0
- data/app/javascript/modal_stack/controllers/modal_stack_link_controller.js +29 -7
- data/app/javascript/modal_stack/install.js +7 -1
- data/app/javascript/modal_stack/orchestrator.js +132 -3
- data/app/javascript/modal_stack/orchestrator.test.js +264 -2
- data/app/javascript/modal_stack/runtime.js +222 -13
- data/app/javascript/modal_stack/runtime.test.js +151 -0
- data/app/javascript/modal_stack/state.js +338 -39
- data/app/javascript/modal_stack/state.test.js +400 -13
- data/app/views/modal_stack/_dialog.html.erb +1 -0
- data/app/views/modal_stack/_panel.html.erb +4 -0
- data/lib/generators/modal_stack/install/install_generator.rb +18 -4
- data/lib/generators/modal_stack/install/templates/initializer.rb +21 -5
- data/lib/generators/modal_stack/views/templates/_dialog.html.erb +9 -0
- data/lib/generators/modal_stack/views/templates/_panel.html.erb +18 -0
- data/lib/generators/modal_stack/views/views_generator.rb +50 -0
- data/lib/modal_stack/capybara.rb +21 -0
- data/lib/modal_stack/configuration.rb +43 -17
- data/lib/modal_stack/controller_extensions.rb +8 -1
- data/lib/modal_stack/engine.rb +2 -0
- data/lib/modal_stack/helpers/modal_back_link_helper.rb +48 -0
- data/lib/modal_stack/helpers/modal_stack_assets_helper.rb +15 -3
- data/lib/modal_stack/helpers/modal_stack_container_helper.rb +42 -15
- data/lib/modal_stack/turbo_streams_extension.rb +56 -0
- data/lib/modal_stack/version.rb +1 -1
- data/lib/modal_stack.rb +5 -1
- metadata +11 -3
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<%# modal_stack panel template — override this file to customise the HTML
|
|
2
|
+
structure that wraps every modal/drawer/bottom-sheet/confirmation panel.
|
|
3
|
+
Available locals:
|
|
4
|
+
content — SafeBuffer, the yielded view content
|
|
5
|
+
back_button — SafeBuffer | nil, pre-rendered back button (nil when back: false)
|
|
6
|
+
wrapper_attrs — Hash { class:, data:, ...extras } — must stay on the root element
|
|
7
|
+
so the JS runtime can read its data attributes
|
|
8
|
+
size — Symbol :sm | :md | :lg | :xl
|
|
9
|
+
variant — Symbol :modal | :drawer | :bottom_sheet | :confirmation
|
|
10
|
+
dismissible — Boolean
|
|
11
|
+
side — Symbol | nil :left | :right | :top | :bottom
|
|
12
|
+
width — String | nil CSS value (e.g. "42rem")
|
|
13
|
+
height — String | nil CSS value
|
|
14
|
+
transition — Symbol | nil :slide | :fade | :none %>
|
|
15
|
+
<%= content_tag(:div, wrapper_attrs) do %>
|
|
16
|
+
<%= back_button %>
|
|
17
|
+
<%= content %>
|
|
18
|
+
<% end %>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/base"
|
|
5
|
+
|
|
6
|
+
module ModalStack
|
|
7
|
+
module Generators
|
|
8
|
+
class ViewsGenerator < Rails::Generators::Base
|
|
9
|
+
source_root File.expand_path("templates", __dir__)
|
|
10
|
+
|
|
11
|
+
class_option :panel, type: :boolean, default: false,
|
|
12
|
+
desc: "Copy only the panel partial"
|
|
13
|
+
class_option :dialog, type: :boolean, default: false,
|
|
14
|
+
desc: "Copy only the dialog partial"
|
|
15
|
+
|
|
16
|
+
def copy_views
|
|
17
|
+
if options[:panel]
|
|
18
|
+
copy_panel
|
|
19
|
+
elsif options[:dialog]
|
|
20
|
+
copy_dialog
|
|
21
|
+
else
|
|
22
|
+
copy_panel
|
|
23
|
+
copy_dialog
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def show_readme
|
|
28
|
+
say <<~TXT, :green
|
|
29
|
+
|
|
30
|
+
modal_stack views ejected to app/views/modal_stack/.
|
|
31
|
+
|
|
32
|
+
Edit the copied partials to override the default HTML structure.
|
|
33
|
+
The `wrapper_attrs` / `dialog_attrs` locals carry the required
|
|
34
|
+
data attributes — keep them on the root element so the JS runtime
|
|
35
|
+
continues to work.
|
|
36
|
+
TXT
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def copy_panel
|
|
42
|
+
copy_file "_panel.html.erb", "app/views/modal_stack/_panel.html.erb"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def copy_dialog
|
|
46
|
+
copy_file "_dialog.html.erb", "app/views/modal_stack/_dialog.html.erb"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/lib/modal_stack/capybara.rb
CHANGED
|
@@ -14,6 +14,7 @@ module ModalStack
|
|
|
14
14
|
module Capybara
|
|
15
15
|
DIALOG_SELECTOR = "#modal-stack-root"
|
|
16
16
|
LAYER_SELECTOR = '[data-modal-stack-target="layer"]:not([data-leaving])'
|
|
17
|
+
FRAME_SELECTOR = "[data-modal-stack-frame]:not([data-leaving])"
|
|
17
18
|
|
|
18
19
|
# Scope Capybara matchers to a specific layer of the stack.
|
|
19
20
|
#
|
|
@@ -81,5 +82,25 @@ module ModalStack
|
|
|
81
82
|
def modal_stack_depth
|
|
82
83
|
::Capybara.current_session.all(:css, LAYER_SELECTOR, wait: 0).size
|
|
83
84
|
end
|
|
85
|
+
|
|
86
|
+
# Capybara matcher: assert the top layer's path frame depth.
|
|
87
|
+
#
|
|
88
|
+
# expect(page).to have_modal_frames(2)
|
|
89
|
+
#
|
|
90
|
+
# Layers without a path read as a single frame.
|
|
91
|
+
def have_modal_frames(count, **)
|
|
92
|
+
have_css("#{LAYER_SELECTOR}[data-frame-depth=\"#{count}\"]", **)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Scope Capybara to the *current* frame inside the top (or specified)
|
|
96
|
+
# layer — i.e. the frame that's not animating out.
|
|
97
|
+
def within_modal_frame(depth: nil, **, &)
|
|
98
|
+
layers = ::Capybara.current_session.all(:css, LAYER_SELECTOR, minimum: 1, **)
|
|
99
|
+
layer = depth ? layers[depth - 1] : layers.last
|
|
100
|
+
raise ::Capybara::ElementNotFound, "no modal_stack layer at depth #{depth}" unless layer
|
|
101
|
+
|
|
102
|
+
frame = layer.first(:css, FRAME_SELECTOR, minimum: 1, **)
|
|
103
|
+
::Capybara.current_session.within(frame, &)
|
|
104
|
+
end
|
|
84
105
|
end
|
|
85
106
|
end
|
|
@@ -11,11 +11,16 @@ module ModalStack
|
|
|
11
11
|
# end
|
|
12
12
|
#
|
|
13
13
|
class Configuration
|
|
14
|
-
CSS_PROVIDERS = %i[
|
|
14
|
+
CSS_PROVIDERS = %i[tailwind_v3 tailwind_v4 bootstrap vanilla none].freeze
|
|
15
|
+
# Aliases accepted on input, normalized to a canonical CSS_PROVIDERS value.
|
|
16
|
+
# `:tailwind` predates the v3/v4 split — keep it working, map to v3 (no change
|
|
17
|
+
# in rendered CSS for existing apps).
|
|
18
|
+
CSS_PROVIDER_ALIASES = { tailwind: :tailwind_v3 }.freeze
|
|
15
19
|
ASSETS_MODES = %i[importmap jsbundling sprockets auto].freeze
|
|
16
20
|
VARIANTS = %i[modal drawer bottom_sheet confirmation].freeze
|
|
17
21
|
SIZES = %i[sm md lg xl].freeze
|
|
18
22
|
MAX_DEPTH_STRATEGIES = %i[raise warn silent].freeze
|
|
23
|
+
PATH_TRANSITIONS = %i[slide fade none].freeze
|
|
19
24
|
|
|
20
25
|
attr_accessor :default_classes,
|
|
21
26
|
:request_header,
|
|
@@ -33,29 +38,18 @@ module ModalStack
|
|
|
33
38
|
:default_size,
|
|
34
39
|
:default_dismissible,
|
|
35
40
|
:max_depth,
|
|
36
|
-
:max_depth_strategy
|
|
41
|
+
:max_depth_strategy,
|
|
42
|
+
:default_path_transition
|
|
37
43
|
|
|
38
44
|
def initialize
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
@default_variant = :modal
|
|
42
|
-
@default_size = :md
|
|
43
|
-
@default_dismissible = true
|
|
44
|
-
@max_depth = 5
|
|
45
|
-
@max_depth_strategy = :warn
|
|
46
|
-
@request_header = "X-Modal-Stack-Request"
|
|
47
|
-
@dialog_id = "modal-stack-root"
|
|
48
|
-
@stack_root_data_attribute = "modal-stack"
|
|
49
|
-
@respect_reduced_motion = true
|
|
50
|
-
@replace_turbo_confirm = false
|
|
51
|
-
@i18n_scope = "modal_stack"
|
|
52
|
-
@initializer_version = nil
|
|
53
|
-
@silence_initializer_warning = false
|
|
45
|
+
apply_behavior_defaults
|
|
46
|
+
apply_naming_defaults
|
|
54
47
|
@default_classes = default_classes_hash
|
|
55
48
|
end
|
|
56
49
|
|
|
57
50
|
def css_provider=(value)
|
|
58
51
|
value = value.to_sym
|
|
52
|
+
value = CSS_PROVIDER_ALIASES.fetch(value, value)
|
|
59
53
|
raise ArgumentError, "css_provider must be one of #{CSS_PROVIDERS.inspect}, got #{value.inspect}" unless CSS_PROVIDERS.include?(value)
|
|
60
54
|
|
|
61
55
|
@css_provider = value
|
|
@@ -110,8 +104,40 @@ module ModalStack
|
|
|
110
104
|
@max_depth_strategy = value
|
|
111
105
|
end
|
|
112
106
|
|
|
107
|
+
def default_path_transition=(value)
|
|
108
|
+
value = value.to_sym
|
|
109
|
+
unless PATH_TRANSITIONS.include?(value)
|
|
110
|
+
raise ArgumentError,
|
|
111
|
+
"default_path_transition must be one of #{PATH_TRANSITIONS.inspect}, got #{value.inspect}"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
@default_path_transition = value
|
|
115
|
+
end
|
|
116
|
+
|
|
113
117
|
private
|
|
114
118
|
|
|
119
|
+
def apply_behavior_defaults
|
|
120
|
+
@css_provider = :tailwind_v3
|
|
121
|
+
@assets_mode = :auto
|
|
122
|
+
@default_variant = :modal
|
|
123
|
+
@default_size = :md
|
|
124
|
+
@default_dismissible = true
|
|
125
|
+
@max_depth = 5
|
|
126
|
+
@max_depth_strategy = :warn
|
|
127
|
+
@default_path_transition = :slide
|
|
128
|
+
@respect_reduced_motion = true
|
|
129
|
+
@replace_turbo_confirm = false
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def apply_naming_defaults
|
|
133
|
+
@request_header = "X-Modal-Stack-Request"
|
|
134
|
+
@dialog_id = "modal-stack-root"
|
|
135
|
+
@stack_root_data_attribute = "modal-stack"
|
|
136
|
+
@i18n_scope = "modal_stack"
|
|
137
|
+
@initializer_version = nil
|
|
138
|
+
@silence_initializer_warning = false
|
|
139
|
+
end
|
|
140
|
+
|
|
115
141
|
def default_classes_hash
|
|
116
142
|
{
|
|
117
143
|
modal_panel: nil,
|
|
@@ -5,7 +5,7 @@ module ModalStack
|
|
|
5
5
|
extend ActiveSupport::Concern
|
|
6
6
|
|
|
7
7
|
included do
|
|
8
|
-
helper_method :modal_stack_request
|
|
8
|
+
helper_method :modal_stack_request?, :modal_stack_config
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
class_methods do
|
|
@@ -39,6 +39,13 @@ module ModalStack
|
|
|
39
39
|
end
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
# Request-scoped accessor for `ModalStack.configuration`. Memoized so
|
|
43
|
+
# helpers that read several config values per render hit the global
|
|
44
|
+
# singleton once instead of N times.
|
|
45
|
+
def modal_stack_config
|
|
46
|
+
@modal_stack_config ||= ModalStack.configuration
|
|
47
|
+
end
|
|
48
|
+
|
|
42
49
|
# True when the current request was issued by the modal_stack JS runtime
|
|
43
50
|
# (signaled by the X-Modal-Stack-Request header on the fetch).
|
|
44
51
|
def modal_stack_request?
|
data/lib/modal_stack/engine.rb
CHANGED
|
@@ -9,9 +9,11 @@ module ModalStack
|
|
|
9
9
|
require "modal_stack/helpers/modal_link_helper"
|
|
10
10
|
require "modal_stack/helpers/modal_stack_container_helper"
|
|
11
11
|
require "modal_stack/helpers/modal_stack_assets_helper"
|
|
12
|
+
require "modal_stack/helpers/modal_back_link_helper"
|
|
12
13
|
include ModalStack::Helpers::ModalLinkHelper
|
|
13
14
|
include ModalStack::Helpers::ModalStackContainerHelper
|
|
14
15
|
include ModalStack::Helpers::ModalStackAssetsHelper
|
|
16
|
+
include ModalStack::Helpers::ModalBackLinkHelper
|
|
15
17
|
end
|
|
16
18
|
end
|
|
17
19
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ModalStack
|
|
4
|
+
module Helpers
|
|
5
|
+
# Renders a button (or link) that steps back through the top layer's
|
|
6
|
+
# path. Wired to the `modal-stack-back-link` Stimulus controller, which
|
|
7
|
+
# delegates to `orchestrator.pathBack`.
|
|
8
|
+
#
|
|
9
|
+
# <%= modal_back_link "Back" %>
|
|
10
|
+
# <%= modal_back_link "Back to step 1", steps: 2 %>
|
|
11
|
+
# <%= modal_back_link class: "btn btn-link" do %>
|
|
12
|
+
# <span aria-hidden="true">←</span> Back
|
|
13
|
+
# <% end %>
|
|
14
|
+
module ModalBackLinkHelper
|
|
15
|
+
LINK_CONTROLLER = "modal-stack-back-link"
|
|
16
|
+
LINK_CLICK_ACTION = "click->modal-stack-back-link#trigger"
|
|
17
|
+
|
|
18
|
+
def modal_back_link(name = nil, options = {}, &block) # rubocop:disable Metrics/CyclomaticComplexity
|
|
19
|
+
if block
|
|
20
|
+
options = name.is_a?(Hash) ? name : {}
|
|
21
|
+
name = capture(&block)
|
|
22
|
+
end
|
|
23
|
+
options = options.dup
|
|
24
|
+
steps = Integer(options.delete(:steps) || 1)
|
|
25
|
+
raise ArgumentError, "steps must be a positive integer, got #{steps.inspect}" if steps < 1
|
|
26
|
+
|
|
27
|
+
options[:data] = back_link_data(options.delete(:data) || {}, steps)
|
|
28
|
+
options[:type] ||= "button"
|
|
29
|
+
|
|
30
|
+
button_tag(name || I18n.t("modal_stack.back", default: "Back"), **options)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def back_link_data(existing, steps)
|
|
36
|
+
existing.merge(
|
|
37
|
+
controller: merged_token(existing[:controller], LINK_CONTROLLER),
|
|
38
|
+
action: merged_token(existing[:action], LINK_CLICK_ACTION),
|
|
39
|
+
modal_stack_back_link_steps_value: steps
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def merged_token(existing, addition)
|
|
44
|
+
[existing, addition].compact.join(" ").strip
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -11,7 +11,7 @@ module ModalStack
|
|
|
11
11
|
# Returns an empty SafeBuffer when `config.css_provider = :none`,
|
|
12
12
|
# so apps can call this unconditionally.
|
|
13
13
|
def modal_stack_stylesheet_link_tag(**)
|
|
14
|
-
provider =
|
|
14
|
+
provider = _modal_stack_config.css_provider
|
|
15
15
|
return ActiveSupport::SafeBuffer.new if provider == :none
|
|
16
16
|
|
|
17
17
|
stylesheet_link_tag("modal_stack/#{provider}", **)
|
|
@@ -23,12 +23,12 @@ module ModalStack
|
|
|
23
23
|
# <%= modal_stack_dialog_tag %>
|
|
24
24
|
#
|
|
25
25
|
def modal_stack_dialog_tag(**html_options)
|
|
26
|
-
config =
|
|
26
|
+
config = _modal_stack_config
|
|
27
27
|
attrs = html_options.dup
|
|
28
28
|
attrs[:id] ||= config.dialog_id
|
|
29
29
|
attrs[:data] = build_dialog_data(attrs[:data], config)
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
render partial: "modal_stack/dialog", locals: { dialog_attrs: attrs }
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
# Merges caller-provided data attrs with the gem-managed ones (controller,
|
|
@@ -48,6 +48,18 @@ module ModalStack
|
|
|
48
48
|
def modal_stack_javascript_tag(**)
|
|
49
49
|
ActiveSupport::SafeBuffer.new
|
|
50
50
|
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
# Prefers the request-scoped accessor injected by ControllerExtensions
|
|
55
|
+
# so we hit `ModalStack.configuration` once per request, not per call.
|
|
56
|
+
# Falls back to the global singleton for non-controller render contexts
|
|
57
|
+
# (mailers, ActionCable, isolated view tests).
|
|
58
|
+
def _modal_stack_config
|
|
59
|
+
return modal_stack_config if respond_to?(:modal_stack_config, true)
|
|
60
|
+
|
|
61
|
+
ModalStack.configuration
|
|
62
|
+
end
|
|
51
63
|
end
|
|
52
64
|
end
|
|
53
65
|
end
|
|
@@ -12,24 +12,51 @@ module ModalStack
|
|
|
12
12
|
module ModalStackContainerHelper
|
|
13
13
|
DEFAULT_SIZE = :md
|
|
14
14
|
|
|
15
|
-
def modal_stack_container(size: DEFAULT_SIZE, dismissible: true, variant: :modal, side: nil, width: nil, height: nil,
|
|
16
|
-
&)
|
|
15
|
+
def modal_stack_container(size: DEFAULT_SIZE, dismissible: true, variant: :modal, side: nil, width: nil, height: nil,
|
|
16
|
+
back: false, transition: nil, html: {}, &)
|
|
17
|
+
attrs = build_panel_attrs(size: size, variant: variant, side: side,
|
|
18
|
+
dismissible: dismissible, width: width,
|
|
19
|
+
height: height, transition: transition, html: html)
|
|
20
|
+
body = capture(&)
|
|
21
|
+
back_html = back ? modal_stack_container_back_button : nil
|
|
22
|
+
|
|
23
|
+
render partial: "modal_stack/panel", locals: {
|
|
24
|
+
content: body, back_button: back_html, wrapper_attrs: attrs,
|
|
25
|
+
size: size, variant: variant, dismissible: dismissible,
|
|
26
|
+
side: side, width: width, height: height, transition: transition
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def build_panel_attrs(size:, variant:, side:, dismissible:, width:, height:, transition:, html:)
|
|
17
33
|
classes = ["modal-stack__panel", "modal-stack__panel--#{variant}", "modal-stack__panel--size-#{size}"]
|
|
18
34
|
classes << "modal-stack__panel--side-#{side}" if side
|
|
19
35
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
data = {
|
|
37
|
+
modal_stack_size: size, modal_stack_variant: variant,
|
|
38
|
+
modal_stack_dismissible: dismissible.to_s, modal_stack_side: side,
|
|
39
|
+
modal_stack_width: width, modal_stack_height: height,
|
|
40
|
+
modal_stack_transition: transition
|
|
41
|
+
}.merge(html.fetch(:data, {})).compact
|
|
42
|
+
|
|
43
|
+
{ class: [classes, html[:class]].compact.join(" "), data: data }
|
|
44
|
+
.merge(html.except(:class, :data))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# The back button stays in the DOM at all depths and is hidden by CSS
|
|
48
|
+
# when the layer is on its first frame (`[data-frame-depth="1"]`).
|
|
49
|
+
# That keeps the wizard structure stable across path_to/path_back —
|
|
50
|
+
# no Stimulus state needed.
|
|
51
|
+
def modal_stack_container_back_button
|
|
52
|
+
label = (defined?(I18n) ? I18n.t("modal_stack.back", default: "Back") : "Back")
|
|
53
|
+
button_tag(
|
|
54
|
+
label,
|
|
55
|
+
type: "button",
|
|
56
|
+
class: "modal-stack__panel-back",
|
|
57
|
+
data: { action: "click->modal-stack#pathBack" },
|
|
58
|
+
aria: { label: label }
|
|
59
|
+
)
|
|
33
60
|
end
|
|
34
61
|
end
|
|
35
62
|
end
|
|
@@ -6,6 +6,7 @@ module ModalStack
|
|
|
6
6
|
# so the standard `turbo_stream.foo(...)` form keeps working alongside.
|
|
7
7
|
module TurboStreamsExtension
|
|
8
8
|
HISTORY_MODES = %i[push replace].freeze
|
|
9
|
+
PATH_TRANSITIONS = %i[slide fade none].freeze
|
|
9
10
|
|
|
10
11
|
# Push a new layer on top of the stack. The content is rendered using
|
|
11
12
|
# the same options as Turbo's standard stream actions
|
|
@@ -64,10 +65,65 @@ module ModalStack
|
|
|
64
65
|
turbo_stream_action_tag(:modal_close_all, target: ModalStack::TARGET_ID)
|
|
65
66
|
end
|
|
66
67
|
|
|
68
|
+
# Navigate forward inside the top layer's path. The current frame is
|
|
69
|
+
# cached in memory so a subsequent `modal_path_back` (or browser back)
|
|
70
|
+
# restores it without a network round-trip. Pass `stale: true` (or set
|
|
71
|
+
# `X-Modal-Stack-Stale: true` on the response) to force a refetch on
|
|
72
|
+
# back.
|
|
73
|
+
def modal_path_to(content = nil, url: nil, transition: nil, stale: false, layer_id: nil, **rendering, &)
|
|
74
|
+
template = render_template(ModalStack::TARGET_ID, content, **rendering, &)
|
|
75
|
+
turbo_stream_action_tag(
|
|
76
|
+
:modal_path_to,
|
|
77
|
+
target: ModalStack::TARGET_ID,
|
|
78
|
+
template: template,
|
|
79
|
+
data: modal_data(
|
|
80
|
+
url: url,
|
|
81
|
+
transition: validate_path_transition(resolved_path_transition(transition)),
|
|
82
|
+
stale: stale ? "true" : nil,
|
|
83
|
+
layer_id: layer_id
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Step back through the top layer's path. Defaults to one frame; pass
|
|
89
|
+
# `steps: N` to collapse multiple frames at once. Clamps at the first
|
|
90
|
+
# frame — does not close the layer.
|
|
91
|
+
def modal_path_back(steps: 1, transition: nil)
|
|
92
|
+
n = Integer(steps)
|
|
93
|
+
raise ArgumentError, "steps must be a positive integer, got #{steps.inspect}" if n < 1
|
|
94
|
+
|
|
95
|
+
turbo_stream_action_tag(
|
|
96
|
+
:modal_path_back,
|
|
97
|
+
target: ModalStack::TARGET_ID,
|
|
98
|
+
data: modal_data(
|
|
99
|
+
steps: n,
|
|
100
|
+
transition: validate_path_transition(resolved_path_transition(transition))
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
|
|
67
105
|
private
|
|
68
106
|
|
|
69
107
|
def modal_data(**attrs)
|
|
70
108
|
attrs.compact
|
|
71
109
|
end
|
|
110
|
+
|
|
111
|
+
def validate_path_transition(value)
|
|
112
|
+
return nil if value.nil?
|
|
113
|
+
|
|
114
|
+
sym = value.to_sym
|
|
115
|
+
unless PATH_TRANSITIONS.include?(sym)
|
|
116
|
+
raise ArgumentError, "transition must be one of #{PATH_TRANSITIONS.inspect}, got #{value.inspect}"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
sym
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Falls back to the configured default when a call site doesn't
|
|
123
|
+
# specify a transition. Pass `transition: :none` to disable
|
|
124
|
+
# explicitly.
|
|
125
|
+
def resolved_path_transition(value)
|
|
126
|
+
value || ModalStack.configuration.default_path_transition
|
|
127
|
+
end
|
|
72
128
|
end
|
|
73
129
|
end
|
data/lib/modal_stack/version.rb
CHANGED
data/lib/modal_stack.rb
CHANGED
|
@@ -10,11 +10,15 @@ module ModalStack
|
|
|
10
10
|
# at the application level (config.dialog_id =, config.request_header =).
|
|
11
11
|
TARGET_ID = "modal-stack-root"
|
|
12
12
|
REQUEST_HEADER = "X-Modal-Stack-Request"
|
|
13
|
+
# Response header set by `path_to` controllers to mark the just-rendered
|
|
14
|
+
# frame as stale — the JS runtime refetches instead of restoring from its
|
|
15
|
+
# in-memory cache when the user steps back to it.
|
|
16
|
+
STALE_HEADER = "X-Modal-Stack-Stale"
|
|
13
17
|
|
|
14
18
|
# Bumped when config/initializers/modal_stack.rb gains/loses an option,
|
|
15
19
|
# so apps that haven't regenerated their initializer get a one-line
|
|
16
20
|
# boot warning. Independent from the gem's VERSION.
|
|
17
|
-
INITIALIZER_VERSION = "0.
|
|
21
|
+
INITIALIZER_VERSION = "0.4.0"
|
|
18
22
|
|
|
19
23
|
class << self
|
|
20
24
|
def configuration
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: modal_stack
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Florian Gagnaire
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-05-
|
|
11
|
+
date: 2026-05-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: railties
|
|
@@ -70,8 +70,10 @@ files:
|
|
|
70
70
|
- Rakefile
|
|
71
71
|
- app/assets/javascripts/modal_stack.js
|
|
72
72
|
- app/assets/stylesheets/modal_stack/bootstrap.css
|
|
73
|
-
- app/assets/stylesheets/modal_stack/
|
|
73
|
+
- app/assets/stylesheets/modal_stack/tailwind_v3.css
|
|
74
|
+
- app/assets/stylesheets/modal_stack/tailwind_v4.css
|
|
74
75
|
- app/assets/stylesheets/modal_stack/vanilla.css
|
|
76
|
+
- app/javascript/modal_stack/controllers/modal_stack_back_link_controller.js
|
|
75
77
|
- app/javascript/modal_stack/controllers/modal_stack_controller.js
|
|
76
78
|
- app/javascript/modal_stack/controllers/modal_stack_link_controller.js
|
|
77
79
|
- app/javascript/modal_stack/index.js
|
|
@@ -83,8 +85,13 @@ files:
|
|
|
83
85
|
- app/javascript/modal_stack/state.js
|
|
84
86
|
- app/javascript/modal_stack/state.test.js
|
|
85
87
|
- app/views/layouts/modal.html.erb
|
|
88
|
+
- app/views/modal_stack/_dialog.html.erb
|
|
89
|
+
- app/views/modal_stack/_panel.html.erb
|
|
86
90
|
- lib/generators/modal_stack/install/install_generator.rb
|
|
87
91
|
- lib/generators/modal_stack/install/templates/initializer.rb
|
|
92
|
+
- lib/generators/modal_stack/views/templates/_dialog.html.erb
|
|
93
|
+
- lib/generators/modal_stack/views/templates/_panel.html.erb
|
|
94
|
+
- lib/generators/modal_stack/views/views_generator.rb
|
|
88
95
|
- lib/modal_stack.rb
|
|
89
96
|
- lib/modal_stack/capybara.rb
|
|
90
97
|
- lib/modal_stack/capybara/minitest.rb
|
|
@@ -92,6 +99,7 @@ files:
|
|
|
92
99
|
- lib/modal_stack/configuration.rb
|
|
93
100
|
- lib/modal_stack/controller_extensions.rb
|
|
94
101
|
- lib/modal_stack/engine.rb
|
|
102
|
+
- lib/modal_stack/helpers/modal_back_link_helper.rb
|
|
95
103
|
- lib/modal_stack/helpers/modal_link_helper.rb
|
|
96
104
|
- lib/modal_stack/helpers/modal_stack_assets_helper.rb
|
|
97
105
|
- lib/modal_stack/helpers/modal_stack_container_helper.rb
|