ultimate_turbo_modal 2.1.1 → 2.2.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/.tool-versions +1 -1
- data/CHANGELOG.md +15 -0
- data/CLAUDE.md +190 -4
- data/Gemfile.lock +1 -1
- data/README.md +55 -2
- data/VERSION +1 -1
- data/javascript/index.js +12 -3
- data/javascript/modal_controller.js +52 -3
- data/javascript/package.json +2 -1
- data/javascript/styles/vanilla.css +86 -0
- data/lib/generators/ultimate_turbo_modal/templates/flavors/custom.rb +29 -2
- data/lib/generators/ultimate_turbo_modal/templates/flavors/tailwind.rb +30 -3
- data/lib/generators/ultimate_turbo_modal/templates/flavors/tailwind3.rb +29 -2
- data/lib/generators/ultimate_turbo_modal/templates/flavors/vanilla.rb +31 -3
- data/lib/generators/ultimate_turbo_modal/update_generator.rb +97 -0
- data/lib/ultimate_turbo_modal/base.rb +35 -14
- data/lib/ultimate_turbo_modal.rb +1 -0
- data/script/build_and_release.sh +20 -7
- metadata +7 -99
- data/demo-app/.gitattributes +0 -7
- data/demo-app/.gitignore +0 -34
- data/demo-app/.ruby-version +0 -1
- data/demo-app/.tool-versions +0 -2
- data/demo-app/Gemfile +0 -41
- data/demo-app/Gemfile.lock +0 -248
- data/demo-app/Procfile.dev +0 -3
- data/demo-app/README.md +0 -15
- data/demo-app/Rakefile +0 -6
- data/demo-app/app/assets/builds/.keep +0 -0
- data/demo-app/app/assets/images/.keep +0 -0
- data/demo-app/app/assets/stylesheets/application.css +0 -42
- data/demo-app/app/controllers/application_controller.rb +0 -10
- data/demo-app/app/controllers/concerns/.keep +0 -0
- data/demo-app/app/controllers/concerns/set_flavor.rb +0 -10
- data/demo-app/app/controllers/hide_from_backends_controller.rb +0 -34
- data/demo-app/app/controllers/modal_controller.rb +0 -11
- data/demo-app/app/controllers/posts_controller.rb +0 -69
- data/demo-app/app/controllers/welcome_controller.rb +0 -2
- data/demo-app/app/helpers/application_helper.rb +0 -9
- data/demo-app/app/javascript/application.js +0 -4
- data/demo-app/app/javascript/controllers/application.js +0 -13
- data/demo-app/app/javascript/controllers/dark_mode_controller.js +0 -28
- data/demo-app/app/javascript/controllers/flash_controller.js +0 -9
- data/demo-app/app/javascript/controllers/hello_controller.js +0 -7
- data/demo-app/app/javascript/controllers/index.js +0 -15
- data/demo-app/app/models/application_record.rb +0 -3
- data/demo-app/app/models/concerns/.keep +0 -0
- data/demo-app/app/models/post.rb +0 -4
- data/demo-app/app/views/hide_from_backends/_notice.html.erb +0 -10
- data/demo-app/app/views/hide_from_backends/create.turbo_stream.erb +0 -2
- data/demo-app/app/views/hide_from_backends/new.html.erb +0 -30
- data/demo-app/app/views/layouts/application.html.erb +0 -40
- data/demo-app/app/views/modal/index.html.erb +0 -45
- data/demo-app/app/views/modal/show.html.erb +0 -39
- data/demo-app/app/views/posts/_form.html.erb +0 -36
- data/demo-app/app/views/posts/_post.html.erb +0 -9
- data/demo-app/app/views/posts/edit.html.erb +0 -9
- data/demo-app/app/views/posts/index.html.erb +0 -14
- data/demo-app/app/views/posts/new.html.erb +0 -3
- data/demo-app/app/views/posts/show.html.erb +0 -21
- data/demo-app/app/views/shared/_flash.html.erb +0 -13
- data/demo-app/app/views/welcome/index.html.erb +0 -19
- data/demo-app/bin/bundle +0 -109
- data/demo-app/bin/dev +0 -7
- data/demo-app/bin/rails +0 -4
- data/demo-app/bin/rake +0 -4
- data/demo-app/bin/setup +0 -34
- data/demo-app/config/application.rb +0 -43
- data/demo-app/config/boot.rb +0 -3
- data/demo-app/config/credentials.yml.enc +0 -1
- data/demo-app/config/database.yml +0 -25
- data/demo-app/config/environment.rb +0 -14
- data/demo-app/config/environments/development.rb +0 -51
- data/demo-app/config/environments/production.rb +0 -67
- data/demo-app/config/environments/test.rb +0 -42
- data/demo-app/config/initializers/assets.rb +0 -7
- data/demo-app/config/initializers/content_security_policy.rb +0 -25
- data/demo-app/config/initializers/filter_parameter_logging.rb +0 -8
- data/demo-app/config/initializers/inflections.rb +0 -14
- data/demo-app/config/initializers/new_framework_defaults_8_0.rb +0 -30
- data/demo-app/config/initializers/permissions_policy.rb +0 -11
- data/demo-app/config/initializers/ultimate_turbo_modal.rb +0 -12
- data/demo-app/config/initializers/ultimate_turbo_modal_tailwind.rb +0 -21
- data/demo-app/config/initializers/ultimate_turbo_modal_vanilla.rb +0 -21
- data/demo-app/config/locales/en.yml +0 -33
- data/demo-app/config/puma.rb +0 -41
- data/demo-app/config/routes.rb +0 -9
- data/demo-app/config.ru +0 -6
- data/demo-app/db/migrate/20230331002502_create_posts.rb +0 -10
- data/demo-app/db/migrate/20231031012703_add_post.rb +0 -9
- data/demo-app/db/migrate/20231128141054_add_publish_on_to_posts.rb +0 -5
- data/demo-app/db/schema.rb +0 -22
- data/demo-app/lib/assets/.keep +0 -0
- data/demo-app/lib/tasks/.keep +0 -0
- data/demo-app/log/.keep +0 -0
- data/demo-app/package.json +0 -28
- data/demo-app/postcss.config.js +0 -7
- data/demo-app/public/400.html +0 -114
- data/demo-app/public/404.html +0 -114
- data/demo-app/public/406-unsupported-browser.html +0 -114
- data/demo-app/public/422.html +0 -114
- data/demo-app/public/500.html +0 -114
- data/demo-app/public/apple-touch-icon-precomposed.png +0 -0
- data/demo-app/public/apple-touch-icon.png +0 -0
- data/demo-app/public/favicon.ico +0 -0
- data/demo-app/public/icon.png +0 -0
- data/demo-app/public/icon.svg +0 -3
- data/demo-app/public/img/bootstrap-logo-shadow.png +0 -0
- data/demo-app/public/img/unicat.jpg +0 -0
- data/demo-app/public/img/vanilla.png +0 -0
- data/demo-app/public/robots.txt +0 -1
- data/demo-app/tmp/.keep +0 -0
- data/demo-app/tmp/pids/.keep +0 -0
- data/demo-app/vendor/.keep +0 -0
- data/demo-app/yarn.lock +0 -1022
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f48f99a3169d751eb02f9efe0f357aa70b285097e03438e2d1b27d1693b9b59c
|
4
|
+
data.tar.gz: c333e8c524f3c2967d1ddc484aab887165f574e0969bdd1b1aa6d1a48752b7ba
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: be4aa9c324429196062c98fd8845b484eb2f14f76e96ee2581063b05568f287e3e4f1961e55a5e6d11a2043a3785af13ebaaf1d9f6594abeb3750f785621f631
|
7
|
+
data.tar.gz: aaad99d25ccbd2bceb518a1a85ecf2e28a7e768e61ff478fddfc723aad170ef06cd7f325fc012e405008799f43f90600440cff93e29528c81cfcbb07b8c94184
|
data/.tool-versions
CHANGED
@@ -1,2 +1,2 @@
|
|
1
|
-
ruby 3.
|
1
|
+
ruby 3.3.9
|
2
2
|
nodejs 22.14.0
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,18 @@
|
|
1
|
+
## [2.2.1] - 2025-08-08
|
2
|
+
|
3
|
+
- Added `rails generate ultimate_turbo_modal:update` for easy updates
|
4
|
+
- Exclude demo-app directory from gem package
|
5
|
+
|
6
|
+
## [2.2.0] - 2025-08-07
|
7
|
+
|
8
|
+
- BREAKING: Make sure to re-run the generator `rails generate ultimate_turbo_modal:install` after install.
|
9
|
+
- Fixed transistions that were sometimes not showing
|
10
|
+
- Improved demo app to make it easier to use for development
|
11
|
+
|
12
|
+
## [2.1.2] - 2025-08-06
|
13
|
+
|
14
|
+
- Fixed scroll lock
|
15
|
+
|
1
16
|
## [2.1.1] - 2025-08-05
|
2
17
|
|
3
18
|
- Reduce Rails dependency to only required components (actionpack, activesupport, railties) (#22)
|
data/CLAUDE.md
CHANGED
@@ -4,7 +4,187 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|
4
4
|
|
5
5
|
## Overview
|
6
6
|
|
7
|
-
Ultimate Turbo Modal (UTMR) is a full-featured modal implementation for Rails applications using Turbo, Stimulus, and Hotwire. It consists of both a Ruby gem and an npm package that work together.
|
7
|
+
Ultimate Turbo Modal (UTMR) is a full-featured modal implementation for Rails applications using Turbo, Stimulus, and Hotwire. It consists of both a Ruby gem and an npm package that work together to provide seamless modal functionality with proper focus management, history manipulation, and customizable styling.
|
8
|
+
|
9
|
+
## Architecture
|
10
|
+
|
11
|
+
### High-Level Design
|
12
|
+
|
13
|
+
The system follows a separation of concerns between server-side rendering (Ruby/Rails) and client-side behavior (JavaScript/Stimulus):
|
14
|
+
|
15
|
+
1. **Server-Side (Ruby Gem)**: Handles HTML generation, configuration management, and Rails integration
|
16
|
+
2. **Client-Side (JavaScript Package)**: Manages modal behavior, focus trapping, scroll locking, and Turbo interactions
|
17
|
+
3. **Communication Layer**: Uses Turbo Frames, Turbo Streams, and data attributes to coordinate between server and client
|
18
|
+
|
19
|
+
### Core Components
|
20
|
+
|
21
|
+
#### Ruby Gem Architecture
|
22
|
+
|
23
|
+
- **Module Structure**: `UltimateTurboModal` is the main module that delegates to configuration and instantiates modal classes
|
24
|
+
- **Base Class**: `UltimateTurboModal::Base` extends `Phlex::HTML` for component-based HTML generation
|
25
|
+
- **Configuration System**: Centralized configuration with validation and type checking
|
26
|
+
- **Flavor System**: CSS framework-specific implementations (Tailwind, Vanilla, Custom) that define styling classes
|
27
|
+
- **Rails Integration**: Via Railtie that injects helpers into ActionController and ActionView
|
28
|
+
|
29
|
+
#### JavaScript Architecture
|
30
|
+
|
31
|
+
- **Stimulus Controller**: `modal_controller.js` handles all modal interactions
|
32
|
+
- **Dependencies**:
|
33
|
+
- `el-transition`: For smooth enter/leave animations
|
34
|
+
- `focus-trap`: For accessibility-compliant focus management
|
35
|
+
- `idiomorph`: For intelligent DOM morphing to prevent flicker
|
36
|
+
- **Global Registration**: Modal instance exposed as `window.modal` for programmatic access
|
37
|
+
- **Turbo Integration**: Custom stream actions and frame handling
|
38
|
+
|
39
|
+
## Detailed Implementation
|
40
|
+
|
41
|
+
### Ruby Components
|
42
|
+
|
43
|
+
#### `UltimateTurboModal` Module (`lib/ultimate_turbo_modal.rb`)
|
44
|
+
- Entry point for the gem
|
45
|
+
- Factory method `new` creates modal instances
|
46
|
+
- `modal_class` method dynamically loads flavor classes based on configuration
|
47
|
+
- Extends self for module-level methods
|
48
|
+
|
49
|
+
#### `Base` Class (`lib/ultimate_turbo_modal/base.rb`)
|
50
|
+
- **Inheritance**: `Phlex::HTML` for HTML generation with Ruby DSL
|
51
|
+
- **Mixins**:
|
52
|
+
- `Phlex::DeferredRenderWithMainContent` for content block handling
|
53
|
+
- Dynamic inclusion of Turbo helpers (FramesHelper, StreamsHelper)
|
54
|
+
- **Key Methods**:
|
55
|
+
- `initialize`: Accepts configuration options with defaults from global config
|
56
|
+
- `view_template`: Main rendering method that wraps content in appropriate Turbo tags
|
57
|
+
- `modal`: Orchestrates HTML structure generation
|
58
|
+
- `div_*` methods: Generate specific HTML elements with proper classes and attributes
|
59
|
+
- **Data Attributes**: Passes configuration to JavaScript via data attributes on the container div
|
60
|
+
|
61
|
+
#### `Configuration` Class (`lib/ultimate_turbo_modal/configuration.rb`)
|
62
|
+
- **Options with Validation**:
|
63
|
+
- `flavor`: Symbol/String for CSS framework (default: `:tailwind`)
|
64
|
+
- `close_button`: Boolean for showing close button
|
65
|
+
- `advance`: Boolean for browser history manipulation
|
66
|
+
- `padding`: Boolean or String for content padding
|
67
|
+
- `header`, `header_divider`, `footer_divider`: Boolean display options
|
68
|
+
- `allowed_click_outside_selector`: Array of CSS selectors that won't dismiss modal
|
69
|
+
- **Type Safety**: Each setter validates input types and raises `ArgumentError` on invalid values
|
70
|
+
|
71
|
+
#### Rails Helpers
|
72
|
+
|
73
|
+
##### `ViewHelper` (`helpers/view_helper.rb`)
|
74
|
+
- `modal` method: Renders modal component with current request context
|
75
|
+
- Instantiates `UltimateTurboModal` with passed options
|
76
|
+
|
77
|
+
##### `ControllerHelper` (`helpers/controller_helper.rb`)
|
78
|
+
- `inside_modal?` method: Detects if request is within modal context
|
79
|
+
- Uses `Turbo-Frame` header to determine modal context
|
80
|
+
- Exposed as helper method to views
|
81
|
+
|
82
|
+
##### `StreamHelper` (`helpers/stream_helper.rb`)
|
83
|
+
- `modal` method: Generates Turbo Stream actions for modal control
|
84
|
+
- Supports `:close` and `:hide` messages
|
85
|
+
- Creates custom `modal` stream action with message attribute
|
86
|
+
|
87
|
+
#### Flavor System
|
88
|
+
- Located in generator templates (`lib/generators/ultimate_turbo_modal/templates/flavors/`)
|
89
|
+
- Each flavor defines CSS class constants for modal elements:
|
90
|
+
- `DIV_DIALOG_CLASSES`, `DIV_OVERLAY_CLASSES`, `DIV_OUTER_CLASSES`, etc.
|
91
|
+
- Flavors inherit from `Base` and override class constants
|
92
|
+
- Supports Tailwind (v3 and v4), Vanilla CSS, and Custom implementations
|
93
|
+
|
94
|
+
### JavaScript Components
|
95
|
+
|
96
|
+
#### Modal Controller (`javascript/modal_controller.js`)
|
97
|
+
|
98
|
+
##### Stimulus Configuration
|
99
|
+
- **Targets**: `container`, `content`
|
100
|
+
- **Values**: `advanceUrl`, `allowedClickOutsideSelector`
|
101
|
+
- **Actions**: Responds to keyboard, click, and Turbo events
|
102
|
+
|
103
|
+
##### Lifecycle Methods
|
104
|
+
- **`connect()`**:
|
105
|
+
- Initializes focus trap and scroll lock variables
|
106
|
+
- Shows modal immediately
|
107
|
+
- Sets up popstate listener for browser back button
|
108
|
+
- Exposes controller as `window.modal`
|
109
|
+
- **`disconnect()`**: Cleans up focus trap and global reference
|
110
|
+
|
111
|
+
##### Core Functionality
|
112
|
+
|
113
|
+
###### Modal Display
|
114
|
+
- **`showModal()`**:
|
115
|
+
- Locks body scroll
|
116
|
+
- Triggers enter transition
|
117
|
+
- Activates focus trap after transition
|
118
|
+
- Pushes history state if `advance` is enabled
|
119
|
+
- **`hideModal()`**:
|
120
|
+
- Prevents double-hiding with `hidingModal` flag
|
121
|
+
- Dispatches cancelable `modal:closing` event
|
122
|
+
- Deactivates focus trap
|
123
|
+
- Triggers leave transition
|
124
|
+
- Cleans up DOM and history
|
125
|
+
- Dispatches `modal:closed` event
|
126
|
+
|
127
|
+
###### Focus Management (`#activateFocusTrap()`, `#deactivateFocusTrap()`)
|
128
|
+
- Creates focus trap with sensible defaults
|
129
|
+
- Finds first focusable element or focuses modal itself
|
130
|
+
- Handles errors gracefully without breaking modal
|
131
|
+
- Respects modal's own keyboard/click handlers
|
132
|
+
|
133
|
+
###### Scroll Locking (`#lockBodyScroll()`, `#unlockBodyScroll()`)
|
134
|
+
- Stores current scroll position
|
135
|
+
- Sets body to `position: fixed` to prevent scroll
|
136
|
+
- Restores original overflow and scroll position on unlock
|
137
|
+
- Prevents layout shift during modal display
|
138
|
+
|
139
|
+
###### History Management
|
140
|
+
- Uses data attribute on body to track history state
|
141
|
+
- `#hasHistoryAdvanced()`, `#setHistoryAdvanced()`, `#resetHistoryAdvanced()`
|
142
|
+
- Coordinates with browser back button via popstate listener
|
143
|
+
|
144
|
+
###### Event Handlers
|
145
|
+
- **`submitEnd()`**: Closes modal on successful form submission
|
146
|
+
- **`closeWithKeyboard()`**: ESC key handler
|
147
|
+
- **`outsideModalClicked()`**: Dismisses modal on outside clicks unless allowed selector matches
|
148
|
+
|
149
|
+
###### Version Checking
|
150
|
+
- `#checkVersions()`: Warns about gem/npm version mismatches in development
|
151
|
+
- Helps developers keep packages in sync
|
152
|
+
|
153
|
+
#### Main Package Entry (`javascript/index.js`)
|
154
|
+
|
155
|
+
##### Turbo Stream Actions
|
156
|
+
- Registers custom `modal` stream action
|
157
|
+
- Handles `hide` and `close` messages via `window.modal` reference
|
158
|
+
|
159
|
+
##### Turbo Frame Integration
|
160
|
+
- **`handleTurboFrameMissing`**: Escapes modal on redirects
|
161
|
+
- **`handleTurboBeforeFrameRender`**: Uses Idiomorph for intelligent morphing
|
162
|
+
- Prevents flicker and unwanted animations
|
163
|
+
- Morphs only innerHTML to preserve modal container
|
164
|
+
|
165
|
+
### Modal Lifecycle Flow
|
166
|
+
|
167
|
+
1. **Trigger**: Link/form targets `data-turbo-frame="modal"`
|
168
|
+
2. **Request**: Rails controller renders modal content
|
169
|
+
3. **Response**:
|
170
|
+
- If Turbo Frame request: Wrapped in `<turbo-frame id="modal">`
|
171
|
+
- If Turbo Stream: Wrapped in stream action targeting modal
|
172
|
+
4. **Client Processing**:
|
173
|
+
- Turbo updates modal frame content
|
174
|
+
- Stimulus controller connects and shows modal
|
175
|
+
- Focus trap activates, scroll locks
|
176
|
+
- History state pushed (if enabled)
|
177
|
+
5. **Interaction**:
|
178
|
+
- User interacts with modal content
|
179
|
+
- Form submissions handled via Turbo
|
180
|
+
- ESC key, close button, or outside clicks trigger hiding
|
181
|
+
6. **Dismissal**:
|
182
|
+
- `modal:closing` event fired (cancelable)
|
183
|
+
- Focus trap deactivates
|
184
|
+
- Leave transition plays
|
185
|
+
- DOM cleaned up
|
186
|
+
- History restored
|
187
|
+
- `modal:closed` event fired
|
8
188
|
|
9
189
|
## Project Structure
|
10
190
|
|
@@ -12,13 +192,19 @@ Ultimate Turbo Modal (UTMR) is a full-featured modal implementation for Rails ap
|
|
12
192
|
- `base.rb`: Core modal component (Phlex-based)
|
13
193
|
- `configuration.rb`: Global configuration management
|
14
194
|
- `helpers/`: Rails helpers for views and controllers
|
195
|
+
- `railtie.rb`: Rails integration setup
|
15
196
|
- Generators in `/lib/generators/` for installation
|
16
|
-
|
197
|
+
|
17
198
|
- **JavaScript Package**: Located in `/javascript/`
|
18
199
|
- `modal_controller.js`: Stimulus controller for modal behavior
|
19
|
-
- `index.js`: Main entry point
|
200
|
+
- `index.js`: Main entry point with Turbo integration
|
201
|
+
- `styles/`: CSS files for vanilla styling
|
20
202
|
- Distributed files built to `/javascript/dist/`
|
21
203
|
|
204
|
+
- **Demo Application**: Located in `/demo-app/`
|
205
|
+
- `Procfile.dev`: Development process file for overmind/foreman
|
206
|
+
- `bin/dev`: Development script for starting the demo app
|
207
|
+
|
22
208
|
## Common Development Commands
|
23
209
|
|
24
210
|
### JavaScript Development (run from `/javascript/` directory)
|
@@ -79,4 +265,4 @@ When adding a new option:
|
|
79
265
|
## Testing Approach
|
80
266
|
- JavaScript: No test framework currently set up
|
81
267
|
- Ruby: Use standard Rails testing practices
|
82
|
-
- Manual testing via the demo app
|
268
|
+
- Manual testing via the demo app (located in `./demo-app`)
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -122,9 +122,62 @@ Do not get overwhelmed with all the options. The defaults are sensible.
|
|
122
122
|
- Focus trap for improved accessibility (Tab and Shift+Tab cycle through focusable elements within the modal only)
|
123
123
|
|
124
124
|
|
125
|
-
## Demo
|
125
|
+
## Demo Video
|
126
126
|
|
127
|
-
A
|
127
|
+
A video demo can be seen here: [https://youtu.be/BVRDXLN1I78](https://youtu.be/BVRDXLN1I78).
|
128
|
+
|
129
|
+
### Running the Demo Application
|
130
|
+
|
131
|
+
The repository includes a demo application in the `demo-app` directory that showcases all the features of Ultimate Turbo Modal. To run it locally:
|
132
|
+
|
133
|
+
```bash
|
134
|
+
# Navigate to the demo app directory
|
135
|
+
cd demo-app
|
136
|
+
|
137
|
+
# Install Ruby dependencies
|
138
|
+
bundle install
|
139
|
+
|
140
|
+
# Create and setup the database
|
141
|
+
bin/rails db:create db:migrate db:seed
|
142
|
+
|
143
|
+
# Install JavaScript dependencies
|
144
|
+
yarn install
|
145
|
+
|
146
|
+
# Start the development server
|
147
|
+
bin/dev
|
148
|
+
|
149
|
+
# Open your browser
|
150
|
+
open http://localhost:3000
|
151
|
+
```
|
152
|
+
|
153
|
+
The demo app provides examples of:
|
154
|
+
- Basic modal usage
|
155
|
+
- Different modal configurations
|
156
|
+
- Custom styling options
|
157
|
+
- Various trigger methods
|
158
|
+
- Advanced features like scrollable content and custom footers
|
159
|
+
|
160
|
+
## Updating between minor versions
|
161
|
+
|
162
|
+
To upgrade within the same major version (for example 2.1 → 2.2):
|
163
|
+
|
164
|
+
1. Change the UTMR gem version in your `Gemfile`:
|
165
|
+
|
166
|
+
```ruby
|
167
|
+
gem "ultimate_turbo_modal", "~> 2.2"
|
168
|
+
```
|
169
|
+
|
170
|
+
2. Install updated dependencies:
|
171
|
+
|
172
|
+
```sh
|
173
|
+
bundle install
|
174
|
+
```
|
175
|
+
|
176
|
+
3. Run the update generator:
|
177
|
+
|
178
|
+
```sh
|
179
|
+
bundle exec rails g ultimate_turbo_modal:update
|
180
|
+
```
|
128
181
|
|
129
182
|
## Upgrading from 1.x
|
130
183
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.
|
1
|
+
2.2.1
|
data/javascript/index.js
CHANGED
@@ -8,10 +8,19 @@ Turbo.StreamActions.modal = function () {
|
|
8
8
|
if (message == "close") window.modal?.hide();
|
9
9
|
};
|
10
10
|
|
11
|
+
// Check if the event target is one of our modal Turbo Frames
|
12
|
+
const isModalFrameTarget = (event) => {
|
13
|
+
const target = event?.target;
|
14
|
+
return (
|
15
|
+
target instanceof Element &&
|
16
|
+
target.tagName.toLowerCase() === 'turbo-frame' &&
|
17
|
+
(target.id === 'modal' || target.id === 'modal-inner')
|
18
|
+
);
|
19
|
+
};
|
20
|
+
|
11
21
|
// Escape modal from the backend on redirects
|
12
22
|
const handleTurboFrameMissing = (event) => {
|
13
|
-
if (event.detail.response.redirected &&
|
14
|
-
event.target == document.querySelector("turbo-frame#modal")) {
|
23
|
+
if (event.detail.response.redirected && isModalFrameTarget(event)) {
|
15
24
|
event.preventDefault()
|
16
25
|
event.detail.visit(event.detail.response)
|
17
26
|
}
|
@@ -19,7 +28,7 @@ const handleTurboFrameMissing = (event) => {
|
|
19
28
|
|
20
29
|
// Morph the innerHTML of the modal to prevent flickering and transition animations
|
21
30
|
const handleTurboBeforeFrameRender = (event) => {
|
22
|
-
if (event
|
31
|
+
if (isModalFrameTarget(event)) {
|
23
32
|
event.detail.render = (currentElement, newElement) => {
|
24
33
|
Idiomorph.morph(currentElement, newElement, {
|
25
34
|
morphstyle: 'innerHTML'
|
@@ -6,7 +6,7 @@ import { createFocusTrap } from 'focus-trap';
|
|
6
6
|
const PACKAGE_VERSION = '__PACKAGE_VERSION__';
|
7
7
|
|
8
8
|
export default class extends Controller {
|
9
|
-
static targets = ["container", "content"]
|
9
|
+
static targets = ["container", "content", "overlay", "outer"]
|
10
10
|
static values = {
|
11
11
|
advanceUrl: String,
|
12
12
|
allowedClickOutsideSelector: String
|
@@ -20,6 +20,10 @@ export default class extends Controller {
|
|
20
20
|
// Initialize focus trap instance variable
|
21
21
|
this.focusTrapInstance = null;
|
22
22
|
|
23
|
+
// Store original body styles for scroll lock
|
24
|
+
this.originalBodyOverflow = null;
|
25
|
+
this.scrollPosition = 0;
|
26
|
+
|
23
27
|
this.showModal();
|
24
28
|
|
25
29
|
this.turboFrame = this.element.closest('turbo-frame');
|
@@ -41,7 +45,14 @@ export default class extends Controller {
|
|
41
45
|
}
|
42
46
|
|
43
47
|
showModal() {
|
44
|
-
|
48
|
+
// Lock body scroll
|
49
|
+
this.#lockBodyScroll();
|
50
|
+
|
51
|
+
// Apply transitions to both overlay and outer elements
|
52
|
+
Promise.all([
|
53
|
+
enter(this.overlayTarget),
|
54
|
+
enter(this.outerTarget)
|
55
|
+
]).then(() => {
|
45
56
|
// Activate focus trap after the modal transition is complete
|
46
57
|
this.#activateFocusTrap();
|
47
58
|
});
|
@@ -112,7 +123,14 @@ export default class extends Controller {
|
|
112
123
|
}
|
113
124
|
|
114
125
|
#resetModalElement() {
|
115
|
-
|
126
|
+
// Unlock body scroll
|
127
|
+
this.#unlockBodyScroll();
|
128
|
+
|
129
|
+
// Apply leave transitions to both overlay and outer elements
|
130
|
+
Promise.all([
|
131
|
+
leave(this.overlayTarget),
|
132
|
+
leave(this.outerTarget)
|
133
|
+
]).then(() => {
|
116
134
|
this.turboFrame.removeAttribute("src");
|
117
135
|
this.containerTarget.remove();
|
118
136
|
this.#resetHistoryAdvanced();
|
@@ -187,4 +205,35 @@ export default class extends Controller {
|
|
187
205
|
this.focusTrapInstance = null;
|
188
206
|
}
|
189
207
|
}
|
208
|
+
|
209
|
+
#lockBodyScroll() {
|
210
|
+
// Store the current scroll position
|
211
|
+
this.scrollPosition = window.pageYOffset || document.documentElement.scrollTop;
|
212
|
+
|
213
|
+
// Store the original overflow style
|
214
|
+
this.originalBodyOverflow = document.body.style.overflow;
|
215
|
+
|
216
|
+
// Prevent scrolling on the body
|
217
|
+
document.body.style.overflow = 'hidden';
|
218
|
+
document.body.style.position = 'fixed';
|
219
|
+
document.body.style.top = `-${this.scrollPosition}px`;
|
220
|
+
document.body.style.width = '100%';
|
221
|
+
}
|
222
|
+
|
223
|
+
#unlockBodyScroll() {
|
224
|
+
// Restore the original overflow style
|
225
|
+
if (this.originalBodyOverflow !== null) {
|
226
|
+
document.body.style.overflow = this.originalBodyOverflow;
|
227
|
+
} else {
|
228
|
+
document.body.style.removeProperty('overflow');
|
229
|
+
}
|
230
|
+
|
231
|
+
// Remove position styles
|
232
|
+
document.body.style.removeProperty('position');
|
233
|
+
document.body.style.removeProperty('top');
|
234
|
+
document.body.style.removeProperty('width');
|
235
|
+
|
236
|
+
// Restore the scroll position
|
237
|
+
window.scrollTo(0, this.scrollPosition);
|
238
|
+
}
|
190
239
|
}
|
data/javascript/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "ultimate_turbo_modal",
|
3
|
-
"version": "2.
|
3
|
+
"version": "2.2.0",
|
4
4
|
"description": "The ultimate Turbo / Stimulus / Hotwire modal window for Rails",
|
5
5
|
"main": "dist/ultimate_turbo_modal.min.js",
|
6
6
|
"module": "dist/ultimate_turbo_modal.min.js",
|
@@ -10,6 +10,7 @@
|
|
10
10
|
"scripts": {
|
11
11
|
"update-version": "node scripts/update-version.js",
|
12
12
|
"build": "yarn install && rollup -c",
|
13
|
+
"build:watch": "rollup -c -w",
|
13
14
|
"release": "bash scripts/release-npm.sh"
|
14
15
|
},
|
15
16
|
"repository": {
|
@@ -1,3 +1,7 @@
|
|
1
|
+
.hidden {
|
2
|
+
display: none
|
3
|
+
}
|
4
|
+
|
1
5
|
.dark {
|
2
6
|
.modal-overlay {
|
3
7
|
background-color: rgba(17, 24, 39, 0.8)
|
@@ -177,3 +181,85 @@
|
|
177
181
|
width: 1.25rem;
|
178
182
|
height: 1.25rem;
|
179
183
|
}
|
184
|
+
|
185
|
+
/*
|
186
|
+
Transition utilities for Vanilla flavor
|
187
|
+
These replicate the Tailwind transitions defined in tailwind.rb
|
188
|
+
using plain CSS classes referenced in vanilla.rb
|
189
|
+
*/
|
190
|
+
|
191
|
+
/* Overlay: Enter */
|
192
|
+
.modal-transition-overlay-enter-animation {
|
193
|
+
transition-property: opacity;
|
194
|
+
transition-duration: 300ms; /* duration-300 */
|
195
|
+
transition-timing-function: ease-out; /* ease-out */
|
196
|
+
}
|
197
|
+
|
198
|
+
.modal-transition-overlay-enter-start {
|
199
|
+
opacity: 0; /* opacity-0 */
|
200
|
+
}
|
201
|
+
|
202
|
+
.modal-transition-overlay-enter-end {
|
203
|
+
opacity: 1; /* opacity-100 */
|
204
|
+
}
|
205
|
+
|
206
|
+
/* Overlay: Leave */
|
207
|
+
.modal-transition-overlay-leave-animation {
|
208
|
+
transition-property: opacity;
|
209
|
+
transition-duration: 200ms; /* duration-200 */
|
210
|
+
transition-timing-function: ease-in; /* ease-in */
|
211
|
+
}
|
212
|
+
|
213
|
+
.modal-transition-overlay-leave-start {
|
214
|
+
opacity: 1; /* opacity-100 */
|
215
|
+
}
|
216
|
+
|
217
|
+
.modal-transition-overlay-leave-end {
|
218
|
+
opacity: 0; /* opacity-0 */
|
219
|
+
}
|
220
|
+
|
221
|
+
/* Dialog: Enter */
|
222
|
+
.modal-transition-dialog-enter-animation {
|
223
|
+
transition-property: opacity, transform;
|
224
|
+
transition-duration: 300ms; /* duration-300 */
|
225
|
+
transition-timing-function: ease-out; /* ease-out */
|
226
|
+
}
|
227
|
+
|
228
|
+
.modal-transition-dialog-enter-start {
|
229
|
+
opacity: 0; /* opacity-0 */
|
230
|
+
transform: translateY(1rem) scale(1); /* translate-y-4 */
|
231
|
+
}
|
232
|
+
|
233
|
+
@media (min-width: 640px) {
|
234
|
+
.modal-transition-dialog-enter-start {
|
235
|
+
transform: translateY(0) scale(0.95); /* sm:translate-y-0 sm:scale-95 */
|
236
|
+
}
|
237
|
+
}
|
238
|
+
|
239
|
+
.modal-transition-dialog-enter-end {
|
240
|
+
opacity: 1; /* opacity-100 */
|
241
|
+
transform: translateY(0) scale(1); /* translate-y-0 scale-100 */
|
242
|
+
}
|
243
|
+
|
244
|
+
/* Dialog: Leave */
|
245
|
+
.modal-transition-dialog-leave-animation {
|
246
|
+
transition-property: opacity, transform;
|
247
|
+
transition-duration: 200ms; /* duration-200 */
|
248
|
+
transition-timing-function: ease-in; /* ease-in */
|
249
|
+
}
|
250
|
+
|
251
|
+
.modal-transition-dialog-leave-start {
|
252
|
+
opacity: 1; /* opacity-100 */
|
253
|
+
transform: translateY(0) scale(1); /* translate-y-0 scale-100 */
|
254
|
+
}
|
255
|
+
|
256
|
+
.modal-transition-dialog-leave-end {
|
257
|
+
opacity: 0; /* opacity-0 */
|
258
|
+
transform: translateY(1rem) scale(1); /* translate-y-4 */
|
259
|
+
}
|
260
|
+
|
261
|
+
@media (min-width: 640px) {
|
262
|
+
.modal-transition-dialog-leave-end {
|
263
|
+
transform: translateY(0) scale(0.95); /* sm:translate-y-0 sm:scale-95 */
|
264
|
+
}
|
265
|
+
}
|
@@ -4,9 +4,9 @@
|
|
4
4
|
# TODO: define the classes for each HTML element.
|
5
5
|
module UltimateTurboModal::Flavors
|
6
6
|
class Custom < UltimateTurboModal::Base
|
7
|
-
|
7
|
+
DIV_MODAL_CONTAINER_CLASSES = ""
|
8
8
|
DIV_OVERLAY_CLASSES = ""
|
9
|
-
|
9
|
+
DIV_DIALOG_CLASSES = ""
|
10
10
|
DIV_INNER_CLASSES = ""
|
11
11
|
DIV_CONTENT_CLASSES = ""
|
12
12
|
DIV_MAIN_CLASSES = ""
|
@@ -18,5 +18,32 @@ module UltimateTurboModal::Flavors
|
|
18
18
|
BUTTON_CLOSE_SR_ONLY_CLASSES = ""
|
19
19
|
CLOSE_BUTTON_TAG_CLASSES = ""
|
20
20
|
ICON_CLOSE_CLASSES = ""
|
21
|
+
|
22
|
+
TRANSITIONS = {
|
23
|
+
overlay: {
|
24
|
+
enter: {
|
25
|
+
animation: "",
|
26
|
+
start: "",
|
27
|
+
end: ""
|
28
|
+
},
|
29
|
+
leave: {
|
30
|
+
animation: "",
|
31
|
+
start: "",
|
32
|
+
end: ""
|
33
|
+
}
|
34
|
+
},
|
35
|
+
dialog: {
|
36
|
+
enter: {
|
37
|
+
animation: "",
|
38
|
+
start: "",
|
39
|
+
end: ""
|
40
|
+
},
|
41
|
+
leave: {
|
42
|
+
animation: "",
|
43
|
+
start: "",
|
44
|
+
end: ""
|
45
|
+
}
|
46
|
+
}
|
47
|
+
}
|
21
48
|
end
|
22
49
|
end
|
@@ -3,9 +3,9 @@
|
|
3
3
|
# Tailwind CSS v4
|
4
4
|
module UltimateTurboModal::Flavors
|
5
5
|
class Tailwind < UltimateTurboModal::Base
|
6
|
-
|
7
|
-
DIV_OVERLAY_CLASSES = "fixed inset-0 bg-gray-900/70 transition-opacity dark:bg-gray-900/80"
|
8
|
-
|
6
|
+
DIV_MODAL_CONTAINER_CLASSES = "relative group z-50"
|
7
|
+
DIV_OVERLAY_CLASSES = "fixed inset-0 bg-gray-900/70 transition-opacity dark:bg-gray-900/80 opacity-0"
|
8
|
+
DIV_DIALOG_CLASSES = "fixed inset-0 overflow-y-auto sm:max-w-[80%] md:max-w-3xl sm:mx-auto m-4 opacity-0"
|
9
9
|
DIV_INNER_CLASSES = "flex min-h-full items-start justify-center pt-[10vh] sm:p-4"
|
10
10
|
DIV_CONTENT_CLASSES = "relative transform max-h-screen overflow-hidden rounded-lg bg-white text-left shadow-lg transition-all sm:my-8 sm:max-w-3xl dark:bg-gray-800 dark:text-white"
|
11
11
|
DIV_MAIN_CLASSES = "group-data-[padding=true]:p-4 group-data-[padding=true]:pt-2 overflow-y-auto max-h-[75vh]"
|
@@ -17,5 +17,32 @@ module UltimateTurboModal::Flavors
|
|
17
17
|
BUTTON_CLOSE_SR_ONLY_CLASSES = "sr-only"
|
18
18
|
CLOSE_BUTTON_TAG_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"
|
19
19
|
ICON_CLOSE_CLASSES = "w-5 h-5"
|
20
|
+
|
21
|
+
TRANSITIONS = {
|
22
|
+
overlay: {
|
23
|
+
enter: {
|
24
|
+
animation: "ease-out duration-300",
|
25
|
+
start: "opacity-0",
|
26
|
+
end: "opacity-100"
|
27
|
+
},
|
28
|
+
leave: {
|
29
|
+
animation: "ease-in duration-200",
|
30
|
+
start: "opacity-100",
|
31
|
+
end: "opacity-0"
|
32
|
+
}
|
33
|
+
},
|
34
|
+
dialog: {
|
35
|
+
enter: {
|
36
|
+
animation: "ease-out duration-300",
|
37
|
+
start: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
|
38
|
+
end: "opacity-100 translate-y-0 sm:scale-100"
|
39
|
+
},
|
40
|
+
leave: {
|
41
|
+
animation: "ease-in duration-200",
|
42
|
+
start: "opacity-100 translate-y-0 sm:scale-100",
|
43
|
+
end: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
44
|
+
}
|
45
|
+
}
|
46
|
+
}
|
20
47
|
end
|
21
48
|
end
|