swal_rails 0.3.4 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9c32d3f037cf95eabb9afc8d578ca88927dbbf95cfca901534f74bc11aad2976
4
- data.tar.gz: 3e5bef30b1085080e83c98ec3a97b7da2dbd28ea1d2452546ab3252b9fa7f029
3
+ metadata.gz: 958168a17777ae58b1ce198ab3de6df9945942a5065c7311201fc05bd4ee8714
4
+ data.tar.gz: 756c79ba6c1bf50bf288a934b807ddda9e09a49eb221097092c72ac3f2981c8b
5
5
  SHA512:
6
- metadata.gz: 15fc85547a69d5bbadd35cfe7a2f2c8b7f9e7627e75f14161dcdfda5c9ed23595f4ae856716baaea56d8f9ed21f2ff9ded2a9b2ba1a99dd70d51c82b5cf70c62
7
- data.tar.gz: e7c1aa7c9057d6c4bcc4708259dc38b40096e57b2bc6a870d8dd2bbe0c3cbdf80b19053a75d47649e541524f52b33fbf8df8fa979b64d38920434e068b7a122a
6
+ metadata.gz: aa8b8b60bb0e2c37cd2669d5dbaaa1e14ccbd3c342f97496679b2829294a050be7250ef131bc2d94cb9d469a51daf3f71bd838edc949beefb2040cbb086b13b5
7
+ data.tar.gz: bf4db27a6c234df8aec4e881c99c67578cf575a3c6fdd53092f7f1e827ce01354c2334d677919159cf31a09e2ecc821193cf78f7438409bb89fdcd1e8c71544b
data/CHANGELOG.md CHANGED
@@ -6,6 +6,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.5.0] - 2026-05-27
10
+
11
+ ### Added
12
+ - **npm companion package** (`swal_rails` on npm). The install generator now runs
13
+ `yarn add swal_rails@<version>` (or `npm install`) in jsbundling mode, placing
14
+ the gem's JS runtime in `node_modules/swal_rails` so esbuild / webpack / vite
15
+ can resolve `import "swal_rails"` as a bare specifier — with no bundler
16
+ config required. All sub-paths (`swal_rails/confirm`, `/flash`, `/chain`,
17
+ `/controllers/swal_controller`) are covered via the package `exports` map.
18
+ Closes [#24](https://github.com/Metalzoid/swal_rails/issues/24).
19
+ - **`rake npm:sync`** — copies JS files from `app/assets/javascripts/swal_rails/`
20
+ into `npm/` and bumps `npm/package.json` version to match `SwalRails::VERSION`.
21
+ Run before any npm publish.
22
+ - **`rake npm:publish`** — runs `npm:sync` then `npm publish --access public`.
23
+ - **`_persistent` flash option** — adding `_persistent: true` to a flash entry
24
+ (via `swal_flash :key, msg, _persistent: true` or directly in `flash_map`)
25
+ removes the auto-close timer and forces a visible close button, requiring
26
+ manual dismissal.
27
+
28
+ ### Fixed
29
+ - **Stacked flash clone timing** — `didRender` replaced by `willOpen` + `didOpen`
30
+ for cloning SA2 popups into the stack container. The previous `timer: 1` could
31
+ race the `setTimeout(0)` SA2 uses to schedule `didOpen`, leaving slots empty.
32
+ Timer raised to 50 ms; the original popup is hidden via `willOpen` (opacity 0,
33
+ pointer-events none) so the clone is the only visible element.
34
+ - **Stacked toast CSS scope** — SA2 scopes element rules under
35
+ `div:where(.swal2-container)` (zero-specificity `:where()`). Clones rendered
36
+ outside that container lost color, border-radius, and the close-button layout.
37
+ Added explicit `div.swal2-popup`, `div.swal2-html-container`, and
38
+ `button.swal2-close` rules in `index.css` that apply regardless of scope.
39
+ - **`install_jsbundling` generator** — previously only installed `sweetalert2`;
40
+ now also installs `swal_rails` from the npm registry.
41
+
42
+ ### Changed
43
+ - Release workflow (`.github/workflows/release.yml`) auto-publishes the npm
44
+ package after every gem push via the `NPM_TOKEN` repository secret.
45
+
46
+ ## [0.4.0] - 2026-05-03
47
+
48
+ ### Added
49
+ - **`config.assets_mode`** (`:auto` default; also `:importmap`, `:jsbundling`, `:sprockets`). When set to a non-`:auto` value, the engine skips file-system sniffing at boot.
50
+ - **`config.precompile_strategy`** (`:all` default; `:minimal` opt-in). Setting it to `:minimal` ships only the sweetalert2 variant matching the resolved `assets_mode` (and drops the optional theme files), saving ~700 KB of precompiled assets in `public/assets/`.
51
+ - **`SwalRails::AssetManifest`** module that computes the precompile list. Pure / side-effect-free; covered by spec.
52
+
53
+ ### Changed
54
+ - The `swal_rails.assets` initializer now runs `after: :load_config_initializers` so it can read user-supplied `config.assets_mode` / `config.precompile_strategy`. Default behaviour is unchanged for hosts that don't configure either option.
55
+
56
+ ### Deprecated
57
+ - **`swal_tag`** and **`swal_chain_tag`** view helpers. Both emit inline `<script type="module">` tags that fight CSP and add per-request nonce overhead; prefer the bundled Stimulus controller (`data-controller="swal"` + `data-action="click->swal#fire"`) or the `data-swal-confirm` / `data-swal-steps` attributes. Slated for removal in v1.0; calling either helper now logs an `ActiveSupport::Deprecation` warning.
58
+
9
59
  ## [0.3.4] - 2026-05-01
10
60
 
11
61
  ### Added
data/README.md CHANGED
@@ -9,9 +9,10 @@ Stimulus controller, auto-wired flash messages, Turbo confirm replacement,
9
9
  Ruby view helpers, and full I18n. Everything is configurable.
10
10
 
11
11
  [![CI](https://github.com/Metalzoid/swal_rails/actions/workflows/main.yml/badge.svg)](https://github.com/Metalzoid/swal_rails/actions)
12
- [![Gem Version](https://badge.fury.io/rb/swal_rails.svg)](https://rubygems.org/gems/swal_rails)
13
- [![Ruby](https://img.shields.io/gem/ruby-version/swal_rails?label=ruby)](https://www.ruby-lang.org/)
14
- [![Rails](https://img.shields.io/gem/dv/swal_rails/railties?label=rails)](https://rubyonrails.org/)
12
+ [![Gem Version](https://img.shields.io/gem/v/swal_rails.svg?label=gem)](https://rubygems.org/gems/swal_rails)
13
+ [![Downloads](https://img.shields.io/gem/dt/swal_rails?label=downloads)](https://rubygems.org/gems/swal_rails)
14
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.2-CC342D?logo=ruby&logoColor=white)](https://www.ruby-lang.org/)
15
+ [![Rails](https://img.shields.io/badge/rails-7.2%20%7C%208.0%20%7C%208.1-CC0000?logo=rubyonrails&logoColor=white)](https://rubyonrails.org/)
15
16
  [![SweetAlert2](https://img.shields.io/badge/SweetAlert2-v11.26-3085d6.svg)](https://sweetalert2.github.io/)
16
17
  [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE.txt)
17
18
 
@@ -284,6 +285,19 @@ Signature: `swal_flash(key, messages, mode: nil, delay: nil, now: false, **optio
284
285
  `_stackDelay` meta-keys, extracted by the JS runtime before the options are
285
286
  handed to `Swal.fire` — they never leak into SA2.
286
287
 
288
+ #### Persistent (non-auto-closing) entries
289
+
290
+ Pass `_persistent: true` to any `swal_flash` call — or add it directly to a
291
+ `flash_map` entry — to remove the auto-close timer and force a visible close
292
+ button, requiring the user to dismiss manually:
293
+
294
+ ```ruby
295
+ swal_flash :alert, "Your session will expire soon.", _persistent: true
296
+ ```
297
+
298
+ The runtime strips `_persistent` before calling `Swal.fire`, then deletes
299
+ `timer` / `timerProgressBar` and sets `showCloseButton: true` on the item.
300
+
287
301
  Behind the scenes, the engine serializes the flash into a meta tag
288
302
  (`<meta name="swal-flash" content="...">`) and the JS runtime reads it and
289
303
  calls `Swal.fire(...)` with your per-key options.
@@ -764,7 +778,7 @@ Flash entries are `{ key: "notice", options: { text: "..." } }` for string value
764
778
  Per-mode side effects:
765
779
 
766
780
  - **importmap**: appends `pin "sweetalert2", to: "sweetalert2.esm.all.js"` and `pin "swal_rails", to: "swal_rails/index.js"` to `config/importmap.rb`; appends `import "swal_rails"` to `app/javascript/application.js`.
767
- - **jsbundling**: runs `yarn add sweetalert2@<pinned>` or `npm install sweetalert2@<pinned>` (based on the lockfile present); appends `import "swal_rails"` to `app/javascript/application.js`.
781
+ - **jsbundling**: runs `yarn add sweetalert2@<pinned> swal_rails@<version>` or `npm install sweetalert2@<pinned> swal_rails@<version>` (based on the lockfile present); appends `import "swal_rails"` to `app/javascript/application.js`. The `swal_rails` npm package ships the same JS runtime as the gem, making the bare specifier `import "swal_rails"` resolvable by any bundler (esbuild, webpack, vite, rollup) without additional configuration.
768
782
  - **sprockets**: appends `//= link sweetalert2.js` and `//= link sweetalert2.css` to `app/assets/config/manifest.js`.
769
783
 
770
784
  All append operations are idempotent — running the generator twice is safe.
@@ -947,7 +961,7 @@ right template automatically.
947
961
  └──────────────────────────────────────────────────────────────────────┘
948
962
 
949
963
  ┌─ jsbundling (esbuild / vite / rollup) ───────────────────────────────┐
950
- │ package.json → "sweetalert2": "^11" (your bundler resolves it)
964
+ │ package.json → "sweetalert2": "^11", "swal_rails": "^0.5"
951
965
  │ app/javascript/application.js │
952
966
  │ import "swal_rails" │
953
967
  └──────────────────────────────────────────────────────────────────────┘
data/Rakefile CHANGED
@@ -10,3 +10,42 @@ require "rubocop/rake_task"
10
10
  RuboCop::RakeTask.new
11
11
 
12
12
  task default: %i[spec rubocop]
13
+
14
+ # ---------------------------------------------------------------------------
15
+ # npm companion package tasks
16
+ # ---------------------------------------------------------------------------
17
+ require "fileutils"
18
+ require "json"
19
+
20
+ NPM_JS_SRC = "app/assets/javascripts/swal_rails"
21
+ NPM_JS_DEST = "npm"
22
+
23
+ def npm_sync_files
24
+ require_relative "lib/swal_rails/version"
25
+
26
+ Dir.glob("#{NPM_JS_SRC}/**/*.js").each do |src|
27
+ rel = src.sub("#{NPM_JS_SRC}/", "")
28
+ dest = "#{NPM_JS_DEST}/#{rel}"
29
+ FileUtils.mkdir_p(File.dirname(dest))
30
+ FileUtils.cp(src, dest)
31
+ puts " synced #{rel}"
32
+ end
33
+
34
+ pkg_path = "#{NPM_JS_DEST}/package.json"
35
+ pkg = JSON.parse(File.read(pkg_path))
36
+ pkg["version"] = SwalRails::VERSION
37
+ File.write(pkg_path, "#{JSON.pretty_generate(pkg)}\n")
38
+ puts " version → #{SwalRails::VERSION}"
39
+ end
40
+
41
+ namespace :npm do
42
+ desc "Sync JS files from #{NPM_JS_SRC}/ → #{NPM_JS_DEST}/ and bump version in package.json"
43
+ task(:sync) { npm_sync_files }
44
+
45
+ desc "Publish npm package to registry (runs npm:sync first)"
46
+ task publish: :sync do
47
+ Dir.chdir(NPM_JS_DEST) do
48
+ system("npm publish --access public") || abort("npm publish failed")
49
+ end
50
+ end
51
+ end
@@ -29,6 +29,18 @@ const extractMeta = (queue) => {
29
29
  return { mode, delay };
30
30
  };
31
31
 
32
+ // Per-item: _persistent removes the auto-close timer and ensures the user
33
+ // must dismiss manually via the close button.
34
+ const applyPersistent = (queue) => {
35
+ for (const item of queue) {
36
+ if (!item._persistent) continue;
37
+ delete item._persistent;
38
+ delete item.timer;
39
+ delete item.timerProgressBar;
40
+ item.showCloseButton = true;
41
+ }
42
+ };
43
+
32
44
  const STACK_ID = "swal-rails-stack";
33
45
 
34
46
  const ensureStackContainer = () => {
@@ -85,42 +97,36 @@ const fireStacked = async (Swal, queue, delay) => {
85
97
  Swal.fire({
86
98
  ...opts,
87
99
  toast: true,
88
- // Close the SA2 original immediately; the clone in `slot` persists.
89
- // Animations disabled on the decoy so the clone captures the popup
90
- // in its normal "shown" state (no opacity-0 from close transition).
91
- timer: 1,
92
100
  timerProgressBar: false,
93
101
  showClass: { popup: "", backdrop: "", icon: "" },
94
102
  hideClass: { popup: "", backdrop: "", icon: "" },
95
- didRender: (popup) => {
103
+ // timer:1 races with the setTimeout(0) SA2 uses to schedule didOpen
104
+ // when animations are disabled — didOpen can lose that race and never
105
+ // fire, leaving the slot empty. 50 ms gives the event loop a safe
106
+ // margin while remaining imperceptible to the user.
107
+ timer: 50,
108
+ // Suppress the SA2 original so only our clone in the stack is visible.
109
+ willOpen: (popup) => {
110
+ popup.style.opacity = "0";
111
+ popup.style.pointerEvents = "none";
112
+ },
113
+ // Clone at didOpen: SA2 has applied all inline styles at this point
114
+ // (display:grid, icon classes, close-button grid placement, etc.),
115
+ // so the clone requires no manual fixups.
116
+ didOpen: (popup) => {
96
117
  const clone = popup.cloneNode(true);
118
+ // willOpen set opacity:0 on the original; clear it on the clone.
97
119
  clone.style.opacity = "";
98
- // SA2 only flips `popup.style.display` to `grid` at didOpen. We
99
- // clone at didRender — earlier in the lifecycle — so the inline
100
- // display is missing. Outside SA2's `.swal2-container` the toast
101
- // therefore falls back to `display: block`, which collapses the
102
- // grid layout (`.swal2-toast { grid-template-columns: … }`) and
103
- // ends up rendering icon/title/content stacked vertically — the
104
- // "vachement haute" symptom of stacked flashes.
105
- clone.style.display = "grid";
106
120
  clone
107
121
  .querySelectorAll(".swal2-timer-progress-bar-container")
108
122
  .forEach((e) => e.remove());
109
- // SA2 adds `.swal2-icon-show` only after didOpen, but we clone
110
- // earlier (in didRender) to beat the close animation. Apply it
111
- // manually so the icon's SVG is visibly drawn in the clone.
112
- clone
113
- .querySelectorAll(".swal2-icon")
114
- .forEach((icon) => icon.classList.add("swal2-icon-show"));
115
123
  slot.appendChild(clone);
116
124
  const dismiss = () => {
117
125
  if (slot.isConnected) slot.remove();
118
126
  if (stack.isConnected && stack.children.length === 0)
119
127
  stack.remove();
120
128
  };
121
- clone
122
- .querySelector(".swal2-close")
123
- ?.addEventListener("click", dismiss);
129
+ clone.querySelector(".swal2-close")?.addEventListener("click", dismiss);
124
130
  if (timerMs) setTimeout(dismiss, timerMs);
125
131
  },
126
132
  didClose: () => resolve(),
@@ -151,6 +157,7 @@ export const installFlash = (Swal, config) => {
151
157
  });
152
158
 
153
159
  const meta = extractMeta(queue);
160
+ applyPersistent(queue);
154
161
  const mode = meta.mode || config.flashArrayMode || "sequential";
155
162
  const delay =
156
163
  meta.delay != null
@@ -3,3 +3,51 @@
3
3
  * Imports the vendored SweetAlert2 CSS; override variables in your own stylesheet.
4
4
  */
5
5
  @import "sweetalert2.css";
6
+
7
+ /*
8
+ * SA2 scopes all element styles under `div:where(.swal2-container)` for CSS
9
+ * isolation (zero-specificity :where() = easy to override, but only inside the
10
+ * container). This breaks stacked-toast clones that live outside that scope.
11
+ *
12
+ * Fix: re-declare the affected rules without the container ancestor. Our
13
+ * selectors have higher specificity than SA2's :where()-based ones, so they
14
+ * win everywhere — values are identical for native popups, so no regression.
15
+ * Risk of page pollution is near-zero: .swal2-* class names are distinctive.
16
+ */
17
+ div.swal2-popup {
18
+ color: var(--swal2-color);
19
+ font-size: 1rem;
20
+ border-radius: var(--swal2-border-radius);
21
+ container-name: swal2-popup;
22
+ }
23
+ div.swal2-html-container {
24
+ color: inherit;
25
+ font-weight: normal;
26
+ line-height: normal;
27
+ overflow-wrap: break-word;
28
+ word-break: break-word;
29
+ cursor: initial;
30
+ }
31
+ button.swal2-close {
32
+ z-index: 2;
33
+ align-items: center;
34
+ justify-content: center;
35
+ padding: 0;
36
+ overflow: hidden;
37
+ transition: var(--swal2-close-button-transition);
38
+ border: none;
39
+ border-radius: var(--swal2-border-radius);
40
+ outline: var(--swal2-close-button-outline);
41
+ background: transparent;
42
+ color: var(--swal2-close-button-color);
43
+ font-family: monospace;
44
+ cursor: pointer;
45
+ }
46
+ button.swal2-close:hover {
47
+ background: transparent;
48
+ color: #f27474;
49
+ }
50
+ button.swal2-close:focus-visible {
51
+ outline: none;
52
+ box-shadow: var(--swal2-close-button-focus-box-shadow);
53
+ }
@@ -99,8 +99,13 @@ module SwalRails
99
99
 
100
100
  def install_jsbundling
101
101
  if file_exists?("package.json")
102
- run "yarn add sweetalert2@#{SwalRails::SWEETALERT2_VERSION}" if file_exists?("yarn.lock")
103
- run "npm install sweetalert2@#{SwalRails::SWEETALERT2_VERSION}" if file_exists?("package-lock.json") && !file_exists?("yarn.lock")
102
+ if file_exists?("yarn.lock")
103
+ run "yarn add sweetalert2@#{SwalRails::SWEETALERT2_VERSION}"
104
+ run "yarn add swal_rails@#{SwalRails::VERSION}"
105
+ elsif file_exists?("package-lock.json")
106
+ run "npm install sweetalert2@#{SwalRails::SWEETALERT2_VERSION}"
107
+ run "npm install swal_rails@#{SwalRails::VERSION}"
108
+ end
104
109
  else
105
110
  say_status(:warn, "package.json not found", :yellow)
106
111
  end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwalRails
4
+ # Computes the list of vendored sweetalert2 assets the host should
5
+ # precompile, based on `SwalRails.configuration.assets_mode` and
6
+ # `precompile_strategy`. Kept side-effect-free so it's easy to test
7
+ # without booting a full Rails app.
8
+ module AssetManifest
9
+ GEM_FILES = %w[
10
+ swal_rails/index.js
11
+ swal_rails/confirm.js
12
+ swal_rails/flash.js
13
+ swal_rails/chain.js
14
+ swal_rails/controllers/swal_controller.js
15
+ swal_rails/index.css
16
+ ].freeze
17
+
18
+ THEME_FILES = %w[
19
+ themes/bootstrap-4.css
20
+ themes/bootstrap-5.css
21
+ themes/borderless.css
22
+ themes/bulma.css
23
+ themes/material-ui.css
24
+ themes/minimal.css
25
+ ].freeze
26
+
27
+ ALL_VENDOR_FILES = %w[
28
+ sweetalert2.js
29
+ sweetalert2.min.js
30
+ sweetalert2.all.js
31
+ sweetalert2.all.min.js
32
+ sweetalert2.esm.js
33
+ sweetalert2.esm.min.js
34
+ sweetalert2.esm.all.js
35
+ sweetalert2.esm.all.min.js
36
+ sweetalert2.css
37
+ sweetalert2.min.css
38
+ ].freeze
39
+
40
+ # Returns the list of asset paths to add to `config.assets.precompile`.
41
+ # Always includes the gem's own JS/CSS entrypoints (small, host-facing).
42
+ # Vendored sweetalert2 variants depend on `precompile_strategy`:
43
+ # `:all` → every JS + CSS variant + every theme (legacy)
44
+ # `:minimal` → only the variant matching the resolved assets_mode,
45
+ # plus the canonical CSS bundle
46
+ def self.precompile_for(configuration, app_root: nil)
47
+ strategy = configuration.precompile_strategy
48
+ mode = resolve_assets_mode(configuration.assets_mode, app_root)
49
+
50
+ base = GEM_FILES.dup
51
+ base.concat(vendor_files_for(strategy, mode))
52
+ base.concat(THEME_FILES) if strategy == :all
53
+ base.uniq
54
+ end
55
+
56
+ # When `assets_mode` is `:auto`, sniff the host's filesystem to infer
57
+ # which Rails asset pipeline is in use. Mirrors the heuristic in the
58
+ # install generator (importmap.rb → :importmap, package.json → :jsbundling,
59
+ # else :sprockets) so explicit and auto-detected behaviour match.
60
+ def self.resolve_assets_mode(mode, app_root)
61
+ return mode unless mode == :auto
62
+ return :sprockets unless app_root
63
+
64
+ return :importmap if app_root.join("config/importmap.rb").exist?
65
+ return :jsbundling if app_root.join("package.json").exist?
66
+
67
+ :sprockets
68
+ end
69
+
70
+ def self.vendor_files_for(strategy, mode)
71
+ return ALL_VENDOR_FILES if strategy == :all
72
+
73
+ case mode
74
+ when :importmap
75
+ # Importmap pins `sweetalert2` to the ESM bundle; CSS is sprocketed
76
+ # separately so we ship `index.css` (which @imports the canonical CSS).
77
+ %w[sweetalert2.esm.all.min.js sweetalert2.min.css]
78
+ when :jsbundling
79
+ # JS comes from `npm install sweetalert2`; we only ship the CSS.
80
+ %w[sweetalert2.min.css]
81
+ when :sprockets
82
+ # Classic sprockets users `javascript_include_tag "sweetalert2.all.min"`.
83
+ %w[sweetalert2.all.min.js sweetalert2.min.css]
84
+ else
85
+ ALL_VENDOR_FILES
86
+ end
87
+ end
88
+ end
89
+ end
@@ -12,6 +12,8 @@ module SwalRails
12
12
  class Configuration
13
13
  CONFIRM_MODES = %i[off data_attribute turbo_override both].freeze
14
14
  FLASH_ARRAY_MODES = %i[sequential stacked].freeze
15
+ ASSETS_MODES = %i[auto importmap jsbundling sprockets].freeze
16
+ PRECOMPILE_STRATEGIES = %i[all minimal].freeze
15
17
 
16
18
  attr_accessor :default_options,
17
19
  :flash_keys_as_meta,
@@ -20,7 +22,8 @@ module SwalRails
20
22
  :flash_stack_delay,
21
23
  :initializer_version,
22
24
  :silence_initializer_warning
23
- attr_reader :confirm_mode, :flash_map, :i18n_scope, :flash_array_mode
25
+ attr_reader :confirm_mode, :flash_map, :i18n_scope, :flash_array_mode,
26
+ :assets_mode, :precompile_strategy
24
27
 
25
28
  def initialize
26
29
  @confirm_mode = :data_attribute
@@ -36,6 +39,16 @@ module SwalRails
36
39
  # explicitly in the initializer template silences it.
37
40
  @initializer_version = nil
38
41
  @silence_initializer_warning = false
42
+ # `:auto` lets the engine sniff (importmap.rb, package.json) at boot.
43
+ # Override in your initializer to lock the choice and skip the sniff.
44
+ @assets_mode = :auto
45
+ # `:all` keeps every sweetalert2 variant in the host's precompile
46
+ # list (legacy behaviour, ~970 KB on disk). `:minimal` precompiles
47
+ # only the variant the host's assets_mode actually uses, shaving
48
+ # ~700 KB. Default stays `:all` for backwards compatibility — flip
49
+ # to `:minimal` once you've confirmed your host doesn't reference
50
+ # extra variants directly.
51
+ @precompile_strategy = :all
39
52
  # `focusConfirm` / `returnFocus` are intentionally omitted: SA2 already
40
53
  # defaults both to `true` internally, and passing them explicitly makes
41
54
  # SA2 warn on every toast ("incompatible with toasts"). Listing them
@@ -67,6 +80,22 @@ module SwalRails
67
80
  @i18n_scope = value.to_s
68
81
  end
69
82
 
83
+ def assets_mode=(value)
84
+ value = value.to_sym
85
+ raise ArgumentError, "assets_mode must be one of #{ASSETS_MODES.inspect}" unless ASSETS_MODES.include?(value)
86
+
87
+ @assets_mode = value
88
+ end
89
+
90
+ def precompile_strategy=(value)
91
+ value = value.to_sym
92
+ unless PRECOMPILE_STRATEGIES.include?(value)
93
+ raise ArgumentError, "precompile_strategy must be one of #{PRECOMPILE_STRATEGIES.inspect}, got #{value.inspect}"
94
+ end
95
+
96
+ @precompile_strategy = value
97
+ end
98
+
70
99
  # Replace the full flash map. Prefer editing individual keys via `flash_map[:key] = ...`.
71
100
  def flash_map=(value)
72
101
  raise ArgumentError, "flash_map must be a Hash" unless value.is_a?(Hash)
@@ -8,24 +8,16 @@ module SwalRails
8
8
 
9
9
  config.swal_rails = SwalRails.configuration
10
10
 
11
- initializer "swal_rails.assets" do |app|
11
+ initializer "swal_rails.assets", after: :load_config_initializers do |app|
12
12
  if app.config.respond_to?(:assets)
13
13
  app.config.assets.paths << root.join("vendor/javascript/sweetalert2").to_s
14
14
  app.config.assets.paths << root.join("vendor/stylesheets/sweetalert2").to_s
15
15
  app.config.assets.paths << root.join("app/assets/javascripts").to_s
16
16
  app.config.assets.paths << root.join("app/assets/stylesheets").to_s
17
- app.config.assets.precompile += %w[
18
- sweetalert2.js sweetalert2.min.js
19
- sweetalert2.all.js sweetalert2.all.min.js
20
- sweetalert2.esm.js sweetalert2.esm.min.js
21
- sweetalert2.esm.all.js sweetalert2.esm.all.min.js
22
- sweetalert2.css sweetalert2.min.css
23
- themes/bootstrap-4.css themes/bootstrap-5.css
24
- themes/borderless.css themes/bulma.css
25
- themes/material-ui.css themes/minimal.css
26
- swal_rails/index.js swal_rails/confirm.js swal_rails/flash.js swal_rails/chain.js
27
- swal_rails/controllers/swal_controller.js
28
- ]
17
+ app.config.assets.precompile += SwalRails::AssetManifest.precompile_for(
18
+ SwalRails.configuration,
19
+ app_root: app.root
20
+ )
29
21
  end
30
22
  end
31
23
 
@@ -100,7 +100,18 @@ module SwalRails
100
100
  # When `nonce: true` is passed and ActionView's CSP helper is available,
101
101
  # Rails substitutes the per-request nonce so the tag survives a
102
102
  # `script-src 'self' 'nonce-…'` policy.
103
+ #
104
+ # @deprecated Inline `<script>` tags add per-request CSP nonce overhead
105
+ # and are not cacheable. Prefer the `data-swal-confirm` attribute or
106
+ # the bundled Stimulus controller (`data-controller="swal"
107
+ # data-action="click->swal#fire"`). Slated for removal in v1.0.
103
108
  def swal_tag(options = {}, html_options = {})
109
+ ActiveSupport::Deprecation.new("1.0", "swal_rails").warn(
110
+ "swal_tag is deprecated. Use the bundled Stimulus controller " \
111
+ "(data-controller=\"swal\" data-action=\"click->swal#fire\" " \
112
+ "data-swal-options-value=\"…\") or a data-swal-confirm attribute. " \
113
+ "swal_tag will be removed in swal_rails 1.0."
114
+ )
104
115
  # json_escape neutralizes `</script>`, `<!--`, U+2028 and U+2029 —
105
116
  # the four sequences that can break out of a <script> block.
106
117
  payload = ERB::Util.json_escape(options.to_json)
@@ -120,7 +131,14 @@ module SwalRails
120
131
  # Same XSS hardening and CSP nonce handling as `swal_tag`. Each step is
121
132
  # a full SweetAlert2 options Hash; `onConfirmed:` / `onDenied:` keys
122
133
  # declare nested sub-chains for conditional branching.
134
+ #
135
+ # @deprecated Same trade-offs as `swal_tag`. Prefer `data-swal-steps`
136
+ # on a button/form. Slated for removal in v1.0.
123
137
  def swal_chain_tag(steps, html_options = {})
138
+ ActiveSupport::Deprecation.new("1.0", "swal_rails").warn(
139
+ "swal_chain_tag is deprecated. Use a data-swal-steps attribute on " \
140
+ "the triggering element. swal_chain_tag will be removed in swal_rails 1.0."
141
+ )
124
142
  # Array(hash) destructures a Hash into [[k, v], ...] pairs — wrap
125
143
  # single Hash steps manually so shorthand `swal_chain_tag(title: "Hi")`
126
144
  # produces `[{"title":"Hi"}]`, not `[["title","Hi"]]`.
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SwalRails
4
- VERSION = "0.3.4"
4
+ VERSION = "0.5.0"
5
5
  SWEETALERT2_VERSION = "11.26.24"
6
6
  end
data/lib/swal_rails.rb CHANGED
@@ -29,6 +29,7 @@ module SwalRails
29
29
  end
30
30
 
31
31
  require_relative "swal_rails/configuration"
32
+ require_relative "swal_rails/asset_manifest"
32
33
  require_relative "swal_rails/initializer_version_check"
33
34
  require_relative "swal_rails/helpers"
34
35
  require_relative "swal_rails/engine" if defined?(Rails::Engine)
data/npm/chain.js ADDED
@@ -0,0 +1,60 @@
1
+ // Runs a sequence of SweetAlert2 modals, advancing only on confirm.
2
+ //
3
+ // Semantics (per step):
4
+ // - isDismissed → abort the chain, return false
5
+ // - isConfirmed → run onConfirmed sub-chain if present, else continue
6
+ // - isDenied → run onDenied sub-chain if present, else abort
7
+ //
8
+ // A chain resolves to `true` iff it ran to completion along a path without
9
+ // abort. That boolean is the contract expected by Turbo.setConfirmMethod
10
+ // and by the data-attribute re-dispatch logic in confirm.js.
11
+ export const CHAIN_DEFAULTS = {
12
+ showCancelButton: true,
13
+ focusCancel: true,
14
+ icon: "warning"
15
+ }
16
+
17
+ // JSON-delivered steps cannot ship functions (e.g. inputValidator).
18
+ // `inputExpected` provides a declarative guard for typed confirmations.
19
+ const normalizeStep = (step) => {
20
+ const {
21
+ onConfirmed,
22
+ onDenied,
23
+ inputExpected,
24
+ inputExpectedError,
25
+ ...sa2Options
26
+ } = step || {}
27
+
28
+ if (typeof inputExpected === "string") {
29
+ const expected = inputExpected
30
+ const error = inputExpectedError || `Type "${expected}" to continue`
31
+ sa2Options.inputValidator = (value) => (
32
+ (value || "").trim() === expected ? undefined : error
33
+ )
34
+ }
35
+
36
+ return { onConfirmed, onDenied, sa2Options }
37
+ }
38
+
39
+ export const chainDialogs = async (Swal, steps) => {
40
+ if (!Array.isArray(steps) || steps.length === 0) return true
41
+
42
+ for (const step of steps) {
43
+ // Strip our own control keys — SA2 ignores unknown options, but leaking
44
+ // `onConfirmed`/`onDenied` into the popup options keeps the serialized
45
+ // payload noisy and invites confusion.
46
+ const { onConfirmed, onDenied, sa2Options } = normalizeStep(step)
47
+ const result = await Swal.fire({ ...CHAIN_DEFAULTS, ...sa2Options })
48
+
49
+ if (result.isDismissed) return false
50
+ if (result.isConfirmed) {
51
+ if (Array.isArray(onConfirmed)) return chainDialogs(Swal, onConfirmed)
52
+ continue
53
+ }
54
+ if (result.isDenied) {
55
+ if (Array.isArray(onDenied)) return chainDialogs(Swal, onDenied)
56
+ return false
57
+ }
58
+ }
59
+ return true
60
+ }
data/npm/confirm.js ADDED
@@ -0,0 +1,103 @@
1
+ import { chainDialogs } from "swal_rails/chain"
2
+
3
+ const parseJSON = (value) => {
4
+ if (!value) return null
5
+ try { return JSON.parse(value) } catch { return null }
6
+ }
7
+
8
+ // When Rails serializes `data: { turbo_confirm: { icon: "error" } }`, the
9
+ // attribute value is a JSON string. Detect that, and accept both Object
10
+ // (single-step options) and Array (multi-step chain) shapes.
11
+ const messagePayload = (message) => {
12
+ if (typeof message !== "string") return null
13
+ const trimmed = message.trim()
14
+ if (trimmed[0] !== "{" && trimmed[0] !== "[") return null
15
+ const parsed = parseJSON(trimmed)
16
+ if (Array.isArray(parsed)) return parsed
17
+ if (parsed && typeof parsed === "object") return parsed
18
+ return null
19
+ }
20
+
21
+ const confirmDialog = (Swal, message, element) => {
22
+ const dataset = element?.dataset || {}
23
+ const payload = messagePayload(message)
24
+ const fromMessage = payload && !Array.isArray(payload) ? payload : null
25
+ const text = fromMessage ? undefined : message
26
+
27
+ const options = {
28
+ title: dataset.swalTitle || text || "Are you sure?",
29
+ text: dataset.swalText || (dataset.swalTitle ? text : undefined),
30
+ icon: dataset.swalIcon || "warning",
31
+ showCancelButton: true,
32
+ focusCancel: true
33
+ }
34
+ if (dataset.swalConfirmText) options.confirmButtonText = dataset.swalConfirmText
35
+ if (dataset.swalCancelText) options.cancelButtonText = dataset.swalCancelText
36
+
37
+ // Merge order (later wins): defaults → data-swal-* shortcuts → JSON message
38
+ // (turbo_confirm: {}) → data-swal-options (most specific).
39
+ const extras = parseJSON(dataset.swalOptions) || {}
40
+ return Swal.fire({ ...options, ...(fromMessage || {}), ...extras }).then((result) => result.isConfirmed)
41
+ }
42
+
43
+ // Dispatches to either a multi-step chain or a single-step confirm. Called
44
+ // from both the Turbo override and the data-attribute listener so both
45
+ // paths behave identically.
46
+ const confirmFlow = (Swal, message, element) => {
47
+ const fromDataset = parseJSON(element?.dataset?.swalSteps)
48
+ if (Array.isArray(fromDataset) && fromDataset.length) return chainDialogs(Swal, fromDataset)
49
+
50
+ const payload = messagePayload(message)
51
+ if (Array.isArray(payload) && payload.length) return chainDialogs(Swal, payload)
52
+
53
+ return confirmDialog(Swal, message, element)
54
+ }
55
+
56
+ const installTurboOverride = (Swal) => {
57
+ if (typeof window.Turbo === "undefined") return false
58
+ const handler = (message, element) => confirmFlow(Swal, message, element)
59
+ // Turbo 8.1+ renamed the API to `Turbo.config.forms.confirm`. The legacy
60
+ // `setConfirmMethod` still works but emits a deprecation warning. Prefer
61
+ // the new path, fall back to the old one for older Turbo versions.
62
+ if (window.Turbo.config?.forms) {
63
+ window.Turbo.config.forms.confirm = handler
64
+ return true
65
+ }
66
+ if (typeof window.Turbo.setConfirmMethod === "function") {
67
+ window.Turbo.setConfirmMethod(handler)
68
+ return true
69
+ }
70
+ return false
71
+ }
72
+
73
+ const installDataAttribute = (Swal) => {
74
+ const handler = (event) => {
75
+ const el = event.target.closest("[data-swal-confirm], [data-swal-steps]")
76
+ if (!el) return
77
+ const message = el.getAttribute("data-swal-confirm")
78
+ event.preventDefault()
79
+ event.stopPropagation()
80
+ confirmFlow(Swal, message, el).then((confirmed) => {
81
+ if (!confirmed) return
82
+ el.removeAttribute("data-swal-confirm")
83
+ el.removeAttribute("data-swal-steps")
84
+ if (typeof el.click === "function" && event.type === "click") {
85
+ el.click()
86
+ } else if (el.tagName === "FORM") {
87
+ // requestSubmit() fires the 'submit' event, so Turbo and any UJS
88
+ // handlers stay in the loop — unlike the raw .submit() which skips them.
89
+ if (typeof el.requestSubmit === "function") el.requestSubmit()
90
+ else el.submit()
91
+ }
92
+ })
93
+ }
94
+ document.addEventListener("click", handler, true)
95
+ document.addEventListener("submit", handler, true)
96
+ }
97
+
98
+ export const installConfirm = (Swal, config) => {
99
+ const mode = config.confirmMode || "data_attribute"
100
+ if (mode === "off") return
101
+ if (mode === "turbo_override" || mode === "both") installTurboOverride(Swal)
102
+ if (mode === "data_attribute" || mode === "both") installDataAttribute(Swal)
103
+ }
@@ -0,0 +1,54 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import Swal from "sweetalert2"
3
+ import { chainDialogs } from "swal_rails/chain"
4
+
5
+ // Fire a Swal modal/toast or a multi-step chain from markup.
6
+ //
7
+ // <button data-controller="swal"
8
+ // data-action="click->swal#fire"
9
+ // data-swal-options-value='{"title":"Hi","icon":"info"}'>
10
+ // Ping
11
+ // </button>
12
+ //
13
+ // <button data-controller="swal"
14
+ // data-action="click->swal#chain"
15
+ // data-swal-steps-value='[{"title":"Sure?"},{"title":"Really?"}]'>
16
+ // Ping
17
+ // </button>
18
+ export default class extends Controller {
19
+ static values = {
20
+ options: { type: Object, default: {} },
21
+ steps: { type: Array, default: [] }
22
+ }
23
+
24
+ fire(event) {
25
+ if (this.element.tagName === "A" || this.element.tagName === "BUTTON") {
26
+ event?.preventDefault?.()
27
+ }
28
+ return Swal.fire(this.optionsValue)
29
+ }
30
+
31
+ confirm(event) {
32
+ event?.preventDefault?.()
33
+ const form = event?.target?.closest?.("form") || this.element
34
+ Swal.fire({
35
+ showCancelButton: true,
36
+ focusCancel: true,
37
+ ...this.optionsValue
38
+ }).then((result) => {
39
+ if (result.isConfirmed && form?.tagName === "FORM") {
40
+ typeof form.requestSubmit === "function" ? form.requestSubmit() : form.submit()
41
+ }
42
+ })
43
+ }
44
+
45
+ async chain(event) {
46
+ event?.preventDefault?.()
47
+ const form = event?.target?.closest?.("form") || this.element
48
+ const ok = await chainDialogs(window.Swal || Swal, this.stepsValue)
49
+ if (ok && form?.tagName === "FORM") {
50
+ typeof form.requestSubmit === "function" ? form.requestSubmit() : form.submit()
51
+ }
52
+ return ok
53
+ }
54
+ }
data/npm/flash.js ADDED
@@ -0,0 +1,174 @@
1
+ // Read the meta tag exactly once per page load. Boot() fires on both
2
+ // DOMContentLoaded and turbo:load, so without this guard an array flash
3
+ // would have its fireNext chain launched twice, racing and cascading via
4
+ // Swal.fire's replace-current-popup behavior — only the last message wins.
5
+ const readFlash = () => {
6
+ const el = document.querySelector('meta[name="swal-flash"]');
7
+ if (!el || el.dataset.swalConsumed === "1") return [];
8
+ el.dataset.swalConsumed = "1";
9
+ try {
10
+ return JSON.parse(el.getAttribute("content")) || [];
11
+ } catch {
12
+ return [];
13
+ }
14
+ };
15
+
16
+ // Keys attached by `swal_flash` helper for per-request mode / delay override.
17
+ // Stripped from the options before being passed to Swal.fire so they never
18
+ // leak into SA2.
19
+ const META_KEYS = ["_arrayMode", "_stackDelay"];
20
+
21
+ const extractMeta = (queue) => {
22
+ let mode = null;
23
+ let delay = null;
24
+ for (const item of queue) {
25
+ if (mode === null && item._arrayMode) mode = item._arrayMode;
26
+ if (delay === null && item._stackDelay != null) delay = item._stackDelay;
27
+ for (const k of META_KEYS) delete item[k];
28
+ }
29
+ return { mode, delay };
30
+ };
31
+
32
+ // Per-item: _persistent removes the auto-close timer and ensures the user
33
+ // must dismiss manually via the close button.
34
+ const applyPersistent = (queue) => {
35
+ for (const item of queue) {
36
+ if (!item._persistent) continue;
37
+ delete item._persistent;
38
+ delete item.timer;
39
+ delete item.timerProgressBar;
40
+ item.showCloseButton = true;
41
+ }
42
+ };
43
+
44
+ const STACK_ID = "swal-rails-stack";
45
+
46
+ const ensureStackContainer = () => {
47
+ let el = document.getElementById(STACK_ID);
48
+ if (!el) {
49
+ el = document.createElement("div");
50
+ el.id = STACK_ID;
51
+ // 360px matches SA2's `body.swal2-toast-shown .swal2-container` width;
52
+ // without it the cloned popups inherit `width: 100%` from SA2 and
53
+ // visually span the whole screen.
54
+ el.style.cssText = [
55
+ "position:fixed",
56
+ "top:1rem",
57
+ "right:1rem",
58
+ "width:360px",
59
+ "max-width:calc(100vw - 2rem)",
60
+ "display:flex",
61
+ "flex-direction:column",
62
+ "gap:.5rem",
63
+ "z-index:10000",
64
+ "pointer-events:none",
65
+ ].join(";");
66
+ document.body.appendChild(el);
67
+ }
68
+ return el;
69
+ };
70
+
71
+ const fireSequential = (Swal, queue) => {
72
+ const fireNext = () => {
73
+ const opts = queue.shift();
74
+ if (!opts) return;
75
+ Swal.fire(opts).then(fireNext);
76
+ };
77
+ fireNext();
78
+ };
79
+
80
+ // SA2 is singleton — two concurrent Swal.fire calls collapse into one
81
+ // popup (the second replaces the first). To stack multiple toasts we let
82
+ // SA2 render each popup, clone it into its own slot, then close the
83
+ // original fast so the next fire is unblocked. The clones live on in our
84
+ // stack with their own timer and click-to-dismiss handlers. Empiler des
85
+ // modales bloquantes n'a pas de sens — on force toast: true.
86
+ const fireStacked = async (Swal, queue, delay) => {
87
+ const stack = ensureStackContainer();
88
+ for (let i = 0; i < queue.length; i++) {
89
+ const opts = queue[i];
90
+ const slot = document.createElement("div");
91
+ slot.className = "swal-rails-stack-slot";
92
+ slot.style.cssText = "width:100%;pointer-events:auto;";
93
+ stack.appendChild(slot);
94
+
95
+ const timerMs = opts.timer;
96
+ await new Promise((resolve) => {
97
+ Swal.fire({
98
+ ...opts,
99
+ toast: true,
100
+ timerProgressBar: false,
101
+ showClass: { popup: "", backdrop: "", icon: "" },
102
+ hideClass: { popup: "", backdrop: "", icon: "" },
103
+ // timer:1 races with the setTimeout(0) SA2 uses to schedule didOpen
104
+ // when animations are disabled — didOpen can lose that race and never
105
+ // fire, leaving the slot empty. 50 ms gives the event loop a safe
106
+ // margin while remaining imperceptible to the user.
107
+ timer: 50,
108
+ // Suppress the SA2 original so only our clone in the stack is visible.
109
+ willOpen: (popup) => {
110
+ popup.style.opacity = "0";
111
+ popup.style.pointerEvents = "none";
112
+ },
113
+ // Clone at didOpen: SA2 has applied all inline styles at this point
114
+ // (display:grid, icon classes, close-button grid placement, etc.),
115
+ // so the clone requires no manual fixups.
116
+ didOpen: (popup) => {
117
+ const clone = popup.cloneNode(true);
118
+ // willOpen set opacity:0 on the original; clear it on the clone.
119
+ clone.style.opacity = "";
120
+ clone
121
+ .querySelectorAll(".swal2-timer-progress-bar-container")
122
+ .forEach((e) => e.remove());
123
+ slot.appendChild(clone);
124
+ const dismiss = () => {
125
+ if (slot.isConnected) slot.remove();
126
+ if (stack.isConnected && stack.children.length === 0)
127
+ stack.remove();
128
+ };
129
+ clone.querySelector(".swal2-close")?.addEventListener("click", dismiss);
130
+ if (timerMs) setTimeout(dismiss, timerMs);
131
+ },
132
+ didClose: () => resolve(),
133
+ });
134
+ });
135
+
136
+ if (i < queue.length - 1 && delay > 0) {
137
+ await new Promise((r) => setTimeout(r, delay));
138
+ }
139
+ }
140
+ };
141
+
142
+ export const installFlash = (Swal, config) => {
143
+ const flashes = readFlash();
144
+ if (!flashes.length) return;
145
+
146
+ const map = config.flashMap || {};
147
+ const queue = flashes.map((flash) => {
148
+ const spec = map[flash.key] ||
149
+ map[flash.key.toLowerCase()] || {
150
+ icon: "info",
151
+ toast: true,
152
+ position: "top-end",
153
+ timer: 3000,
154
+ };
155
+ // Per-request options win over the per-key defaults from flash_map.
156
+ return { ...spec, ...(flash.options || {}) };
157
+ });
158
+
159
+ const meta = extractMeta(queue);
160
+ applyPersistent(queue);
161
+ const mode = meta.mode || config.flashArrayMode || "sequential";
162
+ const delay =
163
+ meta.delay != null
164
+ ? meta.delay
165
+ : config.flashStackDelay != null
166
+ ? config.flashStackDelay
167
+ : 500;
168
+
169
+ if (mode === "stacked" && queue.length > 1) {
170
+ fireStacked(Swal, queue, delay);
171
+ } else {
172
+ fireSequential(Swal, queue);
173
+ }
174
+ };
data/npm/index.js ADDED
@@ -0,0 +1,69 @@
1
+ import Swal from "sweetalert2"
2
+ import { installConfirm } from "swal_rails/confirm"
3
+ import { installFlash } from "swal_rails/flash"
4
+
5
+ const readMeta = (name) => {
6
+ const el = document.querySelector(`meta[name="${name}"]`)
7
+ if (!el) return null
8
+ try { return JSON.parse(el.getAttribute("content")) } catch { return null }
9
+ }
10
+
11
+ const prefersReducedMotion = () =>
12
+ window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches
13
+
14
+ const buildMixin = (config) => {
15
+ const base = { ...(config.defaultOptions || {}) }
16
+ if (config.respectReducedMotion && prefersReducedMotion()) {
17
+ base.showClass = { popup: "" }
18
+ base.hideClass = { popup: "" }
19
+ }
20
+ if (config.i18n?.confirm_button_text) base.confirmButtonText = config.i18n.confirm_button_text
21
+ if (config.i18n?.cancel_button_text) base.cancelButtonText = config.i18n.cancel_button_text
22
+ if (config.i18n?.deny_button_text) base.denyButtonText = config.i18n.deny_button_text
23
+ if (config.i18n?.close_button_aria_label) base.closeButtonAriaLabel = config.i18n.close_button_aria_label
24
+ return base
25
+ }
26
+
27
+ // Module-scoped so repeated calls to boot() (DOMContentLoaded + every
28
+ // turbo:load) don't stack a new click/submit listener per navigation.
29
+ let booted = null
30
+
31
+ const boot = () => {
32
+ if (!booted) {
33
+ const config = readMeta("swal-config") || {}
34
+ const Mixin = Swal.mixin(buildMixin(config))
35
+
36
+ if (config.exposeWindowSwal !== false) {
37
+ window.Swal = Mixin
38
+ }
39
+
40
+ installConfirm(Mixin, config)
41
+ booted = { Swal: Mixin, config }
42
+ document.dispatchEvent(new CustomEvent("swal-rails:ready", { detail: booted }))
43
+ }
44
+
45
+ // Flash meta is re-rendered per request, so read and fire on every page.
46
+ installFlash(booted.Swal, booted.config)
47
+ return booted.Swal
48
+ }
49
+
50
+ const ready = (fn) => {
51
+ if (document.readyState === "loading") {
52
+ document.addEventListener("DOMContentLoaded", fn, { once: true })
53
+ } else {
54
+ fn()
55
+ }
56
+ }
57
+
58
+ ready(boot)
59
+ // `turbo:load` covers full Turbo Drive navigations + initial page loads.
60
+ // `turbo:render` additionally covers form submissions that render with
61
+ // a non-redirect status (e.g. 422 unprocessable_entity for `flash.now`)
62
+ // — Turbo replaces the body but does NOT fire turbo:load in that path.
63
+ // The `data-swal-consumed` guard on the meta tag dedupes the double-fire
64
+ // on full navigations where both events run.
65
+ document.addEventListener("turbo:load", boot)
66
+ document.addEventListener("turbo:render", boot)
67
+
68
+ export { Swal }
69
+ export default Swal
data/npm/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "swal_rails",
3
+ "version": "0.5.0",
4
+ "description": "SweetAlert2 for Rails 7+ — batteries included (jsbundling companion)",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "module": "index.js",
8
+ "exports": {
9
+ ".": "./index.js",
10
+ "./confirm": "./confirm.js",
11
+ "./flash": "./flash.js",
12
+ "./chain": "./chain.js",
13
+ "./controllers/swal_controller": "./controllers/swal_controller.js"
14
+ },
15
+ "files": [
16
+ "*.js",
17
+ "controllers/"
18
+ ],
19
+ "peerDependencies": {
20
+ "sweetalert2": ">=11.0.0"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/Metalzoid/swal_rails"
25
+ },
26
+ "homepage": "https://github.com/Metalzoid/swal_rails",
27
+ "bugs": {
28
+ "url": "https://github.com/Metalzoid/swal_rails/issues"
29
+ },
30
+ "license": "MIT",
31
+ "author": "Florian Gagnaire <gagnaire.flo@gmail.com>",
32
+ "keywords": [
33
+ "rails",
34
+ "sweetalert2",
35
+ "flash",
36
+ "confirm",
37
+ "jsbundling",
38
+ "esbuild",
39
+ "webpack",
40
+ "vite"
41
+ ]
42
+ }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: swal_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.4
4
+ version: 0.5.0
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-01 00:00:00.000000000 Z
11
+ date: 2026-05-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: railties
@@ -70,11 +70,18 @@ files:
70
70
  - lib/generators/swal_rails/install/templates/initializer.rb
71
71
  - lib/generators/swal_rails/locales/locales_generator.rb
72
72
  - lib/swal_rails.rb
73
+ - lib/swal_rails/asset_manifest.rb
73
74
  - lib/swal_rails/configuration.rb
74
75
  - lib/swal_rails/engine.rb
75
76
  - lib/swal_rails/helpers.rb
76
77
  - lib/swal_rails/initializer_version_check.rb
77
78
  - lib/swal_rails/version.rb
79
+ - npm/chain.js
80
+ - npm/confirm.js
81
+ - npm/controllers/swal_controller.js
82
+ - npm/flash.js
83
+ - npm/index.js
84
+ - npm/package.json
78
85
  - vendor/javascript/sweetalert2/LICENSE
79
86
  - vendor/javascript/sweetalert2/sweetalert2.all.js
80
87
  - vendor/javascript/sweetalert2/sweetalert2.all.min.js