moderntw_confirms 1.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 699d66807d16cb4cc6fefb0cc5ce9e0fc9b2cebeb1f4675d0199fac4ce7d63b6
4
+ data.tar.gz: dc52fe74b4c4af10f0615a559c3b24ccdb03570f14bc5e486f83c02de65924e3
5
+ SHA512:
6
+ metadata.gz: ea3e7d14c539f7e6bad0dad77228dc856c5d93ce1174129b3707eb0d7afe2324f6ca122737185345cecaed96f9dcd5599de4452f77eb9efe57d7c83552613f38
7
+ data.tar.gz: 5726f6f98b8962720b67b13a81a26dc8c04f77b362dbc75768b959c0fa85a9d4f65de960fb468eec45106c9556f3197a3f063ec7c3ec2f60873896edc764dad8
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/CHANGELOG.md ADDED
@@ -0,0 +1,24 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2025-10-21
11
+
12
+ ### Added
13
+ - Initial release
14
+ - Automatic interception of Rails `data-confirm` attributes
15
+ - Support for Turbo `data-turbo-confirm` attributes
16
+ - Beautiful Tailwind CSS modal design
17
+ - Smooth fade and scale animations
18
+ - Keyboard navigation support (ESC to cancel, Enter to confirm)
19
+ - Rails generator for easy installation
20
+ - Support for importmap, Webpacker, and Asset Pipeline
21
+ - Optional Stimulus controller for advanced customization
22
+ - Configurable Tailwind classes via initializer
23
+ - Dynamic content support (works with AJAX/Turbo loaded content)
24
+ - Focus management for accessibility
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Robin Ciubotaru
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,173 @@
1
+ # ModernTW Confirms
2
+
3
+ Beautiful Tailwind CSS confirmation modals for Rails applications. Drop-in replacement for browser confirmation dialogs.
4
+
5
+ [![Gem Version](https://badge.fury.io/rb/moderntw_confirms.svg)](https://badge.fury.io/rb/moderntw_confirms)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ ## Features
9
+
10
+ - **Zero Configuration** - Works with your existing Rails code immediately
11
+ - **Beautiful Design** - Tailwind CSS powered modals with smooth animations
12
+ - **Smart Detection** - Automatically styles destructive actions in red
13
+ - **Fully Responsive** - Works perfectly on mobile, tablet, and desktop
14
+ - **Turbo Ready** - Full support for Rails 7+ Turbo and Turbo Streams
15
+ - **Accessible** - Keyboard navigation, focus management, and ARIA labels
16
+ - **Production Ready** - Error boundaries with automatic fallback to native confirms
17
+ - **Customizable** - Easy to modify styles to match your design system
18
+
19
+ ## Installation
20
+
21
+ Add this line to your application's Gemfile:
22
+
23
+ ```ruby
24
+ gem 'moderntw_confirms'
25
+ ```
26
+
27
+ And then execute:
28
+
29
+ ```bash
30
+ bundle install
31
+ ```
32
+
33
+ Run the installation generator:
34
+
35
+ ```bash
36
+ rails generate moderntw_confirms:install
37
+ ```
38
+
39
+ This will:
40
+ - Add the modal partial to your layouts
41
+ - Configure JavaScript based on your setup (Importmap, Webpacker, or Asset Pipeline)
42
+ - Create an optional configuration initializer
43
+
44
+ ## Usage
45
+
46
+ After installation, all your existing confirmation dialogs will automatically use the new modals. No code changes required!
47
+
48
+ ### Existing code that just works:
49
+
50
+ ```erb
51
+ <!-- Links with confirmations -->
52
+ <%= link_to "Delete", item_path(@item),
53
+ method: :delete,
54
+ data: { confirm: "Are you sure?" } %>
55
+
56
+ <!-- Buttons with confirmations -->
57
+ <%= button_to "Remove", item_path(@item),
58
+ method: :delete,
59
+ data: { turbo_confirm: "Delete this item?" } %>
60
+
61
+ <!-- Forms with confirmations -->
62
+ <%= form_with model: @user,
63
+ data: { turbo_confirm: "Save changes?" } do |form| %>
64
+ <!-- form fields -->
65
+ <% end %>
66
+ ```
67
+
68
+ ### Smart Styling
69
+
70
+ The gem automatically detects destructive actions and styles them appropriately:
71
+
72
+ - **Red modals** for: delete, remove, destroy, reset, clear, cancel
73
+ - **Blue modals** for: save, update, submit, confirm, proceed
74
+
75
+ ## Customization
76
+
77
+ ### Quick Styling Changes
78
+
79
+ After installation, the modal HTML lives in `app/views/shared/_moderntw_confirms_modal.html.erb`. You can directly edit:
80
+
81
+ ```erb
82
+ <!-- Change colors, sizes, spacing as needed -->
83
+ <div class="rounded-2xl bg-white p-6">
84
+ <!-- Your customizations here -->
85
+ </div>
86
+ ```
87
+
88
+ ### Configuration Options
89
+
90
+ Create an initializer to configure the gem:
91
+
92
+ ```ruby
93
+ # config/initializers/moderntw_confirms.rb
94
+ ModerntwConfirms.configure do |config|
95
+ config[:backdrop_class] = "bg-black/50"
96
+ config[:modal_class] = "rounded-3xl shadow-2xl"
97
+ config[:confirm_button_class] = "bg-indigo-600 hover:bg-indigo-700"
98
+ config[:cancel_button_class] = "bg-gray-200 hover:bg-gray-300"
99
+ end
100
+ ```
101
+
102
+ ### Animation Customization
103
+
104
+ The animations use CSS transitions and can be modified in the modal partial:
105
+
106
+ ```css
107
+ /* Smooth fade in/out */
108
+ #moderntw-confirm-modal {
109
+ transition: opacity 300ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
110
+ }
111
+
112
+ /* Bounce effect on modal */
113
+ #moderntw-confirm-modal [data-modal-panel] {
114
+ transition: all 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
115
+ }
116
+ ```
117
+
118
+ ## Browser Support
119
+
120
+ - Chrome/Edge 88+
121
+ - Firefox 78+
122
+ - Safari 14+
123
+ - Mobile browsers (iOS Safari, Chrome Mobile)
124
+
125
+ The modal includes automatic fallback to native browser confirms for older browsers or if JavaScript fails.
126
+
127
+ ## Rails Compatibility
128
+
129
+ - Rails 6.1+ with Webpacker or Asset Pipeline
130
+ - Rails 7.0+ with Importmap, Webpacker, or Asset Pipeline
131
+ - Full Turbo and Turbo Streams support
132
+ - Works with both `data-confirm` (Rails UJS) and `data-turbo-confirm` (Turbo)
133
+
134
+ ## Development
135
+
136
+ After checking out the repo, set up the test app:
137
+
138
+ ```bash
139
+ cd test_confirms_app
140
+ bundle install
141
+ rails server
142
+ ```
143
+
144
+ Visit http://localhost:3000 to see the examples.
145
+
146
+ ### Running Tests
147
+
148
+ ```bash
149
+ bundle exec rspec
150
+ ```
151
+
152
+ ## Contributing
153
+
154
+ Bug reports and pull requests are welcome on GitHub at https://github.com/rcbt17/moderntw_confirms. This project is intended to be a safe, welcoming space for collaboration.
155
+
156
+ 1. Fork it
157
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
158
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
159
+ 4. Push to the branch (`git push origin my-new-feature`)
160
+ 5. Create a new Pull Request
161
+
162
+ ## License
163
+
164
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
165
+
166
+ ## Author
167
+
168
+ **Robin Ciubotaru**
169
+ [GitHub](https://github.com/rcbt17)
170
+
171
+ ## Acknowledgments
172
+
173
+ Built with Rails and Tailwind CSS. Special thanks to the Rails and Hotwire communities for their excellent frameworks.
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,125 @@
1
+ require "rails/generators"
2
+
3
+ module ModerntwConfirms
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ desc "Install ModerntwConfirms and configure your application"
9
+
10
+ def install_javascript
11
+ if using_importmap?
12
+ install_with_importmap
13
+ elsif using_webpacker?
14
+ install_with_webpacker
15
+ else
16
+ install_with_asset_pipeline
17
+ end
18
+ end
19
+
20
+ def copy_modal_partial
21
+ say "Copying modal partial", :green
22
+ copy_file "_moderntw_confirms_modal.html.erb", "app/views/shared/_moderntw_confirms_modal.html.erb"
23
+ end
24
+
25
+ def add_modal_to_layout
26
+ say "Adding modal to application layout", :green
27
+
28
+ layout_file = "app/views/layouts/application.html.erb"
29
+ if File.exist?(Rails.root.join(layout_file))
30
+ insert_into_file layout_file, before: "</body>" do
31
+ "\n <%= render 'shared/moderntw_confirms_modal' %>\n "
32
+ end
33
+ else
34
+ say "Please add <%= render 'shared/moderntw_confirms_modal' %> before </body> in your layout", :yellow
35
+ end
36
+ end
37
+
38
+ def add_stimulus_controller
39
+ if using_stimulus?
40
+ say "Adding Stimulus controller for ModerntwConfirms", :green
41
+ append_to_file "app/javascript/controllers/index.js" do
42
+ <<~JS
43
+
44
+ import ConfirmModalController from "moderntw_confirms/confirm_modal_controller"
45
+ application.register("moderntw-confirms", ConfirmModalController)
46
+ JS
47
+ end
48
+ end
49
+ end
50
+
51
+ def create_initializer
52
+ template "moderntw_confirms.rb", "config/initializers/moderntw_confirms.rb"
53
+ end
54
+
55
+ def display_post_install_message
56
+ say "\n ModerntwConfirms has been installed successfully! 🎉", :green
57
+ say "\n Next steps:", :yellow
58
+ say " 1. Make sure Tailwind CSS is installed and configured in your app"
59
+ say " 2. Restart your Rails server"
60
+ say " 3. All your data-confirm attributes will now use beautiful modals!"
61
+ say "\n Configuration:", :yellow
62
+ say " Edit config/initializers/moderntw_confirms.rb to customize modal styles"
63
+ say "\n Usage:", :yellow
64
+ say " Just use data-confirm as usual:"
65
+ say ' <%= link_to "Delete", item_path(@item), method: :delete, data: { confirm: "Are you sure?" } %>'
66
+ say "\n"
67
+ end
68
+
69
+ private
70
+
71
+ def using_importmap?
72
+ Rails.root.join("config", "importmap.rb").exist?
73
+ end
74
+
75
+ def using_webpacker?
76
+ defined?(Webpacker)
77
+ end
78
+
79
+ def using_stimulus?
80
+ Rails.root.join("app", "javascript", "controllers").exist?
81
+ end
82
+
83
+ def install_with_importmap
84
+ say "Installing ModerntwConfirms with importmap", :green
85
+ append_to_file "config/importmap.rb" do
86
+ <<~RUBY
87
+
88
+ # ModerntwConfirms
89
+ pin "moderntw_confirms", to: "moderntw_confirms.js"
90
+ pin "moderntw_confirms/confirm_modal_controller", to: "moderntw_confirms/confirm_modal_controller.js"
91
+ RUBY
92
+ end
93
+
94
+ append_to_file "app/javascript/application.js" do
95
+ <<~JS
96
+
97
+ // ModerntwConfirms - Replace browser confirms with Tailwind modals
98
+ import "moderntw_confirms"
99
+ JS
100
+ end
101
+ end
102
+
103
+ def install_with_webpacker
104
+ say "Installing ModerntwConfirms with Webpacker", :green
105
+ append_to_file "app/javascript/packs/application.js" do
106
+ <<~JS
107
+
108
+ // ModerntwConfirms - Replace browser confirms with Tailwind modals
109
+ require("moderntw_confirms")
110
+ JS
111
+ end
112
+ end
113
+
114
+ def install_with_asset_pipeline
115
+ say "Installing ModerntwConfirms with Asset Pipeline", :green
116
+ append_to_file "app/assets/javascripts/application.js" do
117
+ <<~JS
118
+
119
+ //= require moderntw_confirms
120
+ JS
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,104 @@
1
+ <div id="moderntw-confirm-modal"
2
+ class="hidden fixed inset-0 z-[99999] overflow-y-auto"
3
+ aria-labelledby="moderntw-modal-title"
4
+ aria-modal="true"
5
+ role="dialog"
6
+ data-turbo-permanent>
7
+
8
+ <div class="fixed inset-0" data-modal-backdrop>
9
+ <div class="absolute inset-0 bg-gray-900/40 backdrop-blur-sm"></div>
10
+ </div>
11
+
12
+ <div class="fixed inset-0 overflow-y-auto">
13
+ <div class="flex min-h-full items-center justify-center p-4">
14
+
15
+ <div class="relative w-full max-w-md transform rounded-2xl bg-white p-0 text-left shadow-2xl transition-all"
16
+ data-modal-panel>
17
+
18
+ <div class="p-6">
19
+ <div class="flex items-start space-x-4">
20
+ <div class="flex-shrink-0">
21
+ <div class="flex h-12 w-12 items-center justify-center rounded-full bg-blue-50" data-modal-icon-wrapper>
22
+ <svg data-modal-icon="info" class="h-6 w-6 text-blue-600" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
23
+ <path stroke-linecap="round" stroke-linejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
24
+ </svg>
25
+ <svg data-modal-icon="danger" class="hidden h-6 w-6 text-red-600" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
26
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
27
+ </svg>
28
+ </div>
29
+ </div>
30
+
31
+ <div class="flex-1 pt-1">
32
+ <h3 class="text-lg font-semibold leading-6 text-gray-900" id="moderntw-modal-title" data-modal-title>
33
+ Confirmation Required
34
+ </h3>
35
+ <div class="mt-2">
36
+ <p class="text-sm text-gray-600" data-modal-message>
37
+ Are you sure you want to continue with this action?
38
+ </p>
39
+ </div>
40
+ </div>
41
+ </div>
42
+
43
+ <div class="mt-6 flex flex-col-reverse gap-3 sm:flex-row sm:justify-end">
44
+ <button type="button"
45
+ data-modal-cancel
46
+ class="inline-flex w-full justify-center rounded-xl border border-gray-300 bg-white px-4 py-2.5 text-sm font-semibold text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 sm:w-auto">
47
+ Cancel
48
+ </button>
49
+ <button type="button"
50
+ data-modal-confirm
51
+ class="inline-flex w-full justify-center rounded-xl bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2 sm:w-auto"
52
+ data-confirm-default-class="inline-flex w-full justify-center rounded-xl bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2 sm:w-auto"
53
+ data-confirm-danger-class="inline-flex w-full justify-center rounded-xl bg-red-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus:outline-none focus:ring-2 focus:ring-red-600 focus:ring-offset-2 sm:w-auto">
54
+ Confirm
55
+ </button>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ </div>
60
+ </div>
61
+ </div>
62
+
63
+ <style>
64
+ #moderntw-confirm-modal {
65
+ transition: opacity 300ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
66
+ will-change: opacity;
67
+ }
68
+
69
+ #moderntw-confirm-modal.modal-showing {
70
+ opacity: 1;
71
+ }
72
+
73
+ #moderntw-confirm-modal:not(.modal-showing) {
74
+ opacity: 0;
75
+ }
76
+
77
+ #moderntw-confirm-modal [data-modal-panel] {
78
+ transition: all 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
79
+ will-change: transform, opacity;
80
+ }
81
+
82
+ #moderntw-confirm-modal.modal-showing [data-modal-panel] {
83
+ transform: scale(1) translateY(0);
84
+ opacity: 1;
85
+ }
86
+
87
+ #moderntw-confirm-modal:not(.modal-showing) [data-modal-panel] {
88
+ transform: scale(0.9) translateY(20px);
89
+ opacity: 0;
90
+ }
91
+
92
+ @supports (backdrop-filter: blur(0px)) or (-webkit-backdrop-filter: blur(0px)) {
93
+ #moderntw-confirm-modal [data-modal-backdrop] > div {
94
+ -webkit-backdrop-filter: blur(12px);
95
+ backdrop-filter: blur(12px);
96
+ }
97
+ }
98
+
99
+ @supports not ((backdrop-filter: blur(0px)) or (-webkit-backdrop-filter: blur(0px))) {
100
+ #moderntw-confirm-modal [data-modal-backdrop] > div {
101
+ background-color: rgba(17, 24, 39, 0.75);
102
+ }
103
+ }
104
+ </style>
@@ -0,0 +1,20 @@
1
+ ModerntwConfirms.configure do |config|
2
+ # Customize the Tailwind classes for the modal components
3
+ # These are the default values - modify them to match your design system
4
+
5
+ # Background overlay classes
6
+ config[:backdrop_class] = "bg-black bg-opacity-50"
7
+
8
+ # Modal container classes
9
+ config[:modal_class] = "bg-white rounded-lg shadow-xl"
10
+
11
+ # Confirm button classes
12
+ config[:confirm_button_class] = "bg-blue-500 hover:bg-blue-600 text-white font-semibold py-2 px-4 rounded"
13
+
14
+ # Cancel button classes
15
+ config[:cancel_button_class] = "bg-gray-300 hover:bg-gray-400 text-gray-800 font-semibold py-2 px-4 rounded"
16
+
17
+ # You can also add custom classes for specific use cases:
18
+ # For delete confirmations, you might want red buttons:
19
+ # config[:delete_confirm_button_class] = "bg-red-500 hover:bg-red-600 text-white font-semibold py-2 px-4 rounded"
20
+ end
@@ -0,0 +1,16 @@
1
+ module ModerntwConfirms
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace ModerntwConfirms
4
+
5
+ initializer "moderntw_confirms.assets" do |app|
6
+ app.config.assets.paths << root.join("vendor", "assets", "javascripts")
7
+ app.config.assets.precompile += %w( moderntw_confirms.js )
8
+ end
9
+
10
+ initializer "moderntw_confirms.importmap", before: "importmap" do |app|
11
+ if defined?(Importmap)
12
+ app.config.importmap.paths << Engine.root.join("config/importmap.rb")
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ModerntwConfirms
4
+ VERSION = "1.0.0"
5
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "moderntw_confirms/version"
4
+ require_relative "moderntw_confirms/engine" if defined?(Rails)
5
+
6
+ module ModerntwConfirms
7
+ class Error < StandardError; end
8
+
9
+ mattr_accessor :config
10
+ self.config = {
11
+ backdrop_class: "bg-black bg-opacity-50",
12
+ modal_class: "bg-white rounded-lg shadow-xl",
13
+ confirm_button_class: "bg-blue-500 hover:bg-blue-600 text-white font-semibold py-2 px-4 rounded",
14
+ cancel_button_class: "bg-gray-300 hover:bg-gray-400 text-gray-800 font-semibold py-2 px-4 rounded"
15
+ }
16
+
17
+ def self.configure
18
+ yield config
19
+ end
20
+ end
@@ -0,0 +1,404 @@
1
+ (function() {
2
+ 'use strict';
3
+
4
+ class ModerntwConfirms {
5
+ constructor() {
6
+ this.modal = null;
7
+ this.currentCallback = null;
8
+ this.focusedElementBeforeModal = null;
9
+ this.initialized = false;
10
+ this.initializationAttempts = 0;
11
+ this.maxInitAttempts = 3;
12
+
13
+ try {
14
+ this.init();
15
+ } catch (error) {
16
+ console.error('ModerntwConfirms: Initialization failed', error);
17
+ this.fallbackToNative();
18
+ }
19
+ }
20
+
21
+ init() {
22
+ if (this.initialized || this.initializationAttempts >= this.maxInitAttempts) {
23
+ return;
24
+ }
25
+
26
+ this.initializationAttempts++;
27
+
28
+ if (document.readyState === 'loading') {
29
+ document.addEventListener('DOMContentLoaded', () => this.setup());
30
+ } else {
31
+ this.setup();
32
+ }
33
+ }
34
+
35
+ setup() {
36
+ try {
37
+ this.modal = document.getElementById('moderntw-confirm-modal');
38
+
39
+ if (!this.modal) {
40
+ if (this.initializationAttempts < this.maxInitAttempts) {
41
+ setTimeout(() => this.init(), 500);
42
+ return;
43
+ } else {
44
+ console.warn('ModerntwConfirms: Modal not found after multiple attempts. Falling back to native confirms.');
45
+ this.fallbackToNative();
46
+ return;
47
+ }
48
+ }
49
+
50
+ this.setupModalElements();
51
+ this.interceptTurboConfirms();
52
+ this.setupModalHandlers();
53
+ this.initialized = true;
54
+
55
+ } catch (error) {
56
+ console.error('ModerntwConfirms: Setup failed', error);
57
+ this.fallbackToNative();
58
+ }
59
+ }
60
+
61
+ fallbackToNative() {
62
+ if (window.Turbo && window.Turbo.config && window.Turbo.config.forms) {
63
+ Turbo.config.forms.confirm = (message) => {
64
+ return Promise.resolve(window.confirm(message));
65
+ };
66
+ }
67
+ }
68
+
69
+ setupModalElements() {
70
+ this.backdrop = this.modal.querySelector('[data-modal-backdrop]');
71
+ this.panel = this.modal.querySelector('[data-modal-panel]');
72
+ this.messageEl = this.modal.querySelector('[data-modal-message]');
73
+ this.titleEl = this.modal.querySelector('[data-modal-title]');
74
+ this.confirmBtn = this.modal.querySelector('[data-modal-confirm]');
75
+ this.cancelBtn = this.modal.querySelector('[data-modal-cancel]');
76
+ this.iconWrapper = this.modal.querySelector('[data-modal-icon-wrapper]');
77
+ this.infoIcon = this.modal.querySelector('[data-modal-icon="info"]');
78
+ this.dangerIcon = this.modal.querySelector('[data-modal-icon="danger"]');
79
+
80
+ if (!this.messageEl || !this.confirmBtn || !this.cancelBtn) {
81
+ throw new Error('Critical modal elements not found');
82
+ }
83
+ }
84
+
85
+ interceptTurboConfirms() {
86
+ const self = this;
87
+
88
+ if (window.Turbo && window.Turbo.config && window.Turbo.config.forms) {
89
+ Turbo.config.forms.confirm = (message) => {
90
+ return new Promise((resolve) => {
91
+ try {
92
+ self.showModal(message, resolve);
93
+ } catch (error) {
94
+ console.error('ModerntwConfirms: Error showing modal', error);
95
+ resolve(window.confirm(message));
96
+ }
97
+ });
98
+ };
99
+ }
100
+
101
+ this.formSubmitHandler = (event) => {
102
+ try {
103
+ const element = event.target;
104
+ const message = element.getAttribute('data-turbo-confirm') || element.getAttribute('data-confirm');
105
+
106
+ if (message && !element.hasAttribute('data-moderntw-confirmed')) {
107
+ event.preventDefault();
108
+
109
+ if (event.detail && event.detail.formSubmission) {
110
+ event.detail.formSubmission.stop();
111
+ }
112
+
113
+ this.showModal(message, (confirmed) => {
114
+ if (confirmed) {
115
+ element.setAttribute('data-moderntw-confirmed', 'true');
116
+ element.requestSubmit();
117
+ setTimeout(() => element.removeAttribute('data-moderntw-confirmed'), 100);
118
+ }
119
+ });
120
+ }
121
+ } catch (error) {
122
+ console.error('ModerntwConfirms: Error in form submit handler', error);
123
+ }
124
+ };
125
+
126
+ document.addEventListener('turbo:submit-start', this.formSubmitHandler);
127
+
128
+ this.clickHandler = (event) => {
129
+ try {
130
+ let element = event.target;
131
+
132
+ while (element && element !== document.body) {
133
+ if (element.hasAttribute('data-turbo-confirm') || element.hasAttribute('data-confirm')) {
134
+ break;
135
+ }
136
+ element = element.parentElement;
137
+ }
138
+
139
+ if (!element || element === document.body) return;
140
+
141
+ const message = element.getAttribute('data-turbo-confirm') || element.getAttribute('data-confirm');
142
+
143
+ if (message && !element.hasAttribute('data-moderntw-confirmed')) {
144
+ event.preventDefault();
145
+ event.stopPropagation();
146
+ event.stopImmediatePropagation();
147
+
148
+ this.showModal(message, (confirmed) => {
149
+ if (confirmed) {
150
+ element.setAttribute('data-moderntw-confirmed', 'true');
151
+
152
+ const turboConfirm = element.getAttribute('data-turbo-confirm');
153
+ const dataConfirm = element.getAttribute('data-confirm');
154
+ element.removeAttribute('data-turbo-confirm');
155
+ element.removeAttribute('data-confirm');
156
+
157
+ if (element.tagName === 'FORM') {
158
+ element.requestSubmit();
159
+ } else if (element.tagName === 'BUTTON' && element.form) {
160
+ element.form.requestSubmit(element);
161
+ } else {
162
+ element.click();
163
+ }
164
+
165
+ setTimeout(() => {
166
+ if (turboConfirm) element.setAttribute('data-turbo-confirm', turboConfirm);
167
+ if (dataConfirm) element.setAttribute('data-confirm', dataConfirm);
168
+ element.removeAttribute('data-moderntw-confirmed');
169
+ }, 100);
170
+ }
171
+ });
172
+
173
+ return false;
174
+ }
175
+ } catch (error) {
176
+ console.error('ModerntwConfirms: Error in click handler', error);
177
+ }
178
+ };
179
+
180
+ document.addEventListener('click', this.clickHandler, true);
181
+ }
182
+
183
+ setupModalHandlers() {
184
+ if (this.cancelBtn) {
185
+ this.cancelBtnHandler = () => this.handleAction(false);
186
+ this.cancelBtn.addEventListener('click', this.cancelBtnHandler);
187
+ }
188
+
189
+ if (this.backdrop) {
190
+ this.backdropHandler = () => this.handleAction(false);
191
+ this.backdrop.addEventListener('click', this.backdropHandler);
192
+ }
193
+
194
+ if (this.confirmBtn) {
195
+ this.confirmBtnHandler = () => this.handleAction(true);
196
+ this.confirmBtn.addEventListener('click', this.confirmBtnHandler);
197
+ }
198
+
199
+ this.escHandler = (e) => {
200
+ if (e.key === 'Escape' && this.isModalOpen()) {
201
+ e.preventDefault();
202
+ this.handleAction(false);
203
+ }
204
+ };
205
+ document.addEventListener('keydown', this.escHandler);
206
+
207
+ this.tabHandler = (e) => {
208
+ if (e.key === 'Tab' && this.isModalOpen()) {
209
+ this.trapFocus(e);
210
+ }
211
+ };
212
+ this.modal.addEventListener('keydown', this.tabHandler);
213
+ }
214
+
215
+ showModal(message, callback) {
216
+ if (!this.modal || !this.initialized) {
217
+ const result = window.confirm(message);
218
+ if (callback) callback(result);
219
+ return;
220
+ }
221
+
222
+ try {
223
+ this.currentCallback = callback;
224
+ this.focusedElementBeforeModal = document.activeElement;
225
+
226
+ if (this.messageEl) {
227
+ this.messageEl.textContent = String(message || 'Are you sure?');
228
+ }
229
+
230
+ const isDanger = /delete|remove|destroy|reset|clear|drop|erase|cancel/i.test(message);
231
+ this.setModalType(isDanger ? 'danger' : 'info');
232
+
233
+ this.modal.classList.remove('hidden');
234
+ this.modal.offsetHeight;
235
+
236
+ requestAnimationFrame(() => {
237
+ this.modal.classList.add('modal-showing');
238
+ });
239
+
240
+ setTimeout(() => {
241
+ if (this.confirmBtn) {
242
+ this.confirmBtn.focus();
243
+ }
244
+ }, 100);
245
+
246
+ document.body.style.overflow = 'hidden';
247
+
248
+ } catch (error) {
249
+ console.error('ModerntwConfirms: Error showing modal', error);
250
+ const result = window.confirm(message);
251
+ if (callback) callback(result);
252
+ }
253
+ }
254
+
255
+ setModalType(type) {
256
+ try {
257
+ const defaultBtnClass = this.confirmBtn?.getAttribute('data-confirm-default-class');
258
+ const dangerBtnClass = this.confirmBtn?.getAttribute('data-confirm-danger-class');
259
+
260
+ if (type === 'danger') {
261
+ if (this.infoIcon) this.infoIcon.classList.add('hidden');
262
+ if (this.dangerIcon) this.dangerIcon.classList.remove('hidden');
263
+ if (this.iconWrapper) {
264
+ this.iconWrapper.className = 'flex h-12 w-12 items-center justify-center rounded-full bg-red-50';
265
+ }
266
+
267
+ if (this.confirmBtn && dangerBtnClass) {
268
+ this.confirmBtn.className = dangerBtnClass;
269
+ }
270
+
271
+ if (this.titleEl) {
272
+ this.titleEl.textContent = 'Are you absolutely sure?';
273
+ }
274
+ } else {
275
+ if (this.infoIcon) this.infoIcon.classList.remove('hidden');
276
+ if (this.dangerIcon) this.dangerIcon.classList.add('hidden');
277
+ if (this.iconWrapper) {
278
+ this.iconWrapper.className = 'flex h-12 w-12 items-center justify-center rounded-full bg-blue-50';
279
+ }
280
+
281
+ if (this.confirmBtn && defaultBtnClass) {
282
+ this.confirmBtn.className = defaultBtnClass;
283
+ }
284
+
285
+ if (this.titleEl) {
286
+ this.titleEl.textContent = 'Confirmation Required';
287
+ }
288
+ }
289
+ } catch (error) {
290
+ console.error('ModerntwConfirms: Error setting modal type', error);
291
+ }
292
+ }
293
+
294
+ handleAction(confirmed) {
295
+ if (!this.modal) return;
296
+
297
+ try {
298
+ this.modal.classList.remove('modal-showing');
299
+
300
+ setTimeout(() => {
301
+ this.modal.classList.add('hidden');
302
+
303
+ document.body.style.overflow = '';
304
+
305
+ if (this.focusedElementBeforeModal && this.focusedElementBeforeModal.focus) {
306
+ try {
307
+ this.focusedElementBeforeModal.focus();
308
+ } catch (e) {
309
+ }
310
+ }
311
+
312
+ if (this.currentCallback) {
313
+ const callback = this.currentCallback;
314
+ this.currentCallback = null;
315
+ callback(confirmed);
316
+ }
317
+ }, 300);
318
+ } catch (error) {
319
+ console.error('ModerntwConfirms: Error handling action', error);
320
+ if (this.currentCallback) {
321
+ this.currentCallback(confirmed);
322
+ }
323
+ }
324
+ }
325
+
326
+ isModalOpen() {
327
+ return this.modal && !this.modal.classList.contains('hidden');
328
+ }
329
+
330
+ trapFocus(e) {
331
+ try {
332
+ const focusableElements = this.modal.querySelectorAll(
333
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
334
+ );
335
+
336
+ if (focusableElements.length === 0) return;
337
+
338
+ const firstFocusable = focusableElements[0];
339
+ const lastFocusable = focusableElements[focusableElements.length - 1];
340
+
341
+ if (e.shiftKey && document.activeElement === firstFocusable) {
342
+ e.preventDefault();
343
+ lastFocusable.focus();
344
+ } else if (!e.shiftKey && document.activeElement === lastFocusable) {
345
+ e.preventDefault();
346
+ firstFocusable.focus();
347
+ }
348
+ } catch (error) {
349
+ console.error('ModerntwConfirms: Error trapping focus', error);
350
+ }
351
+ }
352
+
353
+ destroy() {
354
+ if (this.formSubmitHandler) {
355
+ document.removeEventListener('turbo:submit-start', this.formSubmitHandler);
356
+ }
357
+ if (this.clickHandler) {
358
+ document.removeEventListener('click', this.clickHandler, true);
359
+ }
360
+ if (this.escHandler) {
361
+ document.removeEventListener('keydown', this.escHandler);
362
+ }
363
+ if (this.cancelBtn && this.cancelBtnHandler) {
364
+ this.cancelBtn.removeEventListener('click', this.cancelBtnHandler);
365
+ }
366
+ if (this.backdrop && this.backdropHandler) {
367
+ this.backdrop.removeEventListener('click', this.backdropHandler);
368
+ }
369
+ if (this.confirmBtn && this.confirmBtnHandler) {
370
+ this.confirmBtn.removeEventListener('click', this.confirmBtnHandler);
371
+ }
372
+ if (this.modal && this.tabHandler) {
373
+ this.modal.removeEventListener('keydown', this.tabHandler);
374
+ }
375
+ }
376
+ }
377
+
378
+ if (typeof window !== 'undefined') {
379
+ window.ModerntwConfirms = ModerntwConfirms;
380
+
381
+ const initializeOnce = () => {
382
+ try {
383
+ if (window.moderntwConfirmsInstance && window.moderntwConfirmsInstance.destroy) {
384
+ window.moderntwConfirmsInstance.destroy();
385
+ }
386
+
387
+ window.moderntwConfirmsInstance = new ModerntwConfirms();
388
+ } catch (error) {
389
+ console.error('ModerntwConfirms: Failed to initialize', error);
390
+ }
391
+ };
392
+
393
+ if (window.Turbo) {
394
+ initializeOnce();
395
+ }
396
+
397
+ document.addEventListener('turbo:load', initializeOnce);
398
+ document.addEventListener('DOMContentLoaded', initializeOnce);
399
+
400
+ if (document.readyState !== 'loading') {
401
+ setTimeout(initializeOnce, 100);
402
+ }
403
+ }
404
+ })();
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: moderntw_confirms
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Robin Ciubotaru
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2025-10-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '6.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '6.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: sqlite3
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: puma
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: Automatically intercepts Rails data-confirm and data-turbo-confirm attributes,
56
+ replacing native browser dialogs with customizable Tailwind CSS modals
57
+ email:
58
+ - robinciubotaru17@gmail.com
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - ".rspec"
64
+ - CHANGELOG.md
65
+ - LICENSE.txt
66
+ - README.md
67
+ - Rakefile
68
+ - lib/generators/moderntw_confirms/install/install_generator.rb
69
+ - lib/generators/moderntw_confirms/install/templates/_moderntw_confirms_modal.html.erb
70
+ - lib/generators/moderntw_confirms/install/templates/moderntw_confirms.rb
71
+ - lib/moderntw_confirms.rb
72
+ - lib/moderntw_confirms/engine.rb
73
+ - lib/moderntw_confirms/version.rb
74
+ - vendor/assets/javascripts/moderntw_confirms.js
75
+ homepage: https://github.com/rcbt17/moderntw_confirms
76
+ licenses:
77
+ - MIT
78
+ metadata:
79
+ homepage_uri: https://github.com/rcbt17/moderntw_confirms
80
+ source_code_uri: https://github.com/rcbt17/moderntw_confirms
81
+ changelog_uri: https://github.com/rcbt17/moderntw_confirms/blob/main/CHANGELOG.md
82
+ post_install_message:
83
+ rdoc_options: []
84
+ require_paths:
85
+ - lib
86
+ required_ruby_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: 3.0.0
91
+ required_rubygems_version: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ requirements: []
97
+ rubygems_version: 3.5.9
98
+ signing_key:
99
+ specification_version: 4
100
+ summary: Replace browser confirm dialogs with modern Tailwind CSS modals in Rails
101
+ test_files: []