swal_rails 0.3.1.beta1 → 0.3.2

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: 9e9ac64c06c6de5525bf1964f4c866b72b3a5fc4fb42fed239778cc94e0bc57c
4
- data.tar.gz: abefde5449478e377516fb40b78087fb89b60f73c60788422309e0930e278643
3
+ metadata.gz: d9c8d829f1cd2f384c7a40bde0a723bd3a3fd04dccb6fea15b3bd6c543e2a3a7
4
+ data.tar.gz: d1b4801ab03a209a14051665b0d410c35971dcc2e0bb17ca16d72e724f446a0f
5
5
  SHA512:
6
- metadata.gz: 455ee6bc8175ee826535fcfbb7be404f9b56ca2d2a92715f0fe47d818d10521257bcdb8bd640b7b344e26e980aeaa5948d2d237fa097df0aa0e2c534a6fd337a
7
- data.tar.gz: db9ff1404a3e0b33b0e9b70cb04feb9386c2e94b5dad486477d18b319b16d9c64e055eb47a726f491447bcba326010b08f3565de07f343ad3a0033f6634cc925
6
+ metadata.gz: a72b02157f593c15a9886e2e7c20ebcf6140048cd7f231742096f91215f28cb9273c50b9772405c8d611450d68b89715805f3015393ac15177e28cc981354f22
7
+ data.tar.gz: e4590aa66cbd7429b85b5d4a194911a3c1011144f96903293b40eb02ddc72ebb8153da0c0e0bb6fb0b41b327058bfb6a81e81108a66287845af7e4169fcfcf41
data/CHANGELOG.md CHANGED
@@ -6,15 +6,73 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
- ## [0.3.1.beta1] - 2026-04-21
9
+ ## [0.3.2] - 2026-04-25
10
+
11
+ First stable release on top of the `0.3.1.beta1` + `0.3.1.beta2`
12
+ prereleases. End users upgrading from `0.3.0` should read the
13
+ consolidated entry below.
14
+
15
+ ### Added
16
+ - `config.flash_array_mode` (`:sequential` default | `:stacked`) — how a
17
+ multi-entry flash payload is played. Sequential waits for each Swal to
18
+ close before firing the next; stacked renders every toast in parallel in
19
+ a fixed top-right container with a configurable delay between each
20
+ appearance.
21
+ - `config.flash_stack_delay` (ms, default 500) — gap between stacked
22
+ toasts in `:stacked` mode.
23
+ - `swal_flash(key, messages, mode:, delay:, now:, **options)` helper,
24
+ available in both controllers and views. Lets a single call override the
25
+ global mode/delay and merge extra SA2 options for the payload:
26
+ `swal_flash :alert, @post.errors.full_messages, mode: :stacked, delay: 300`.
27
+ - Reserved meta-keys `_arrayMode` / `_stackDelay` on flash entry options,
28
+ stripped by the JS runtime before being passed to `Swal.fire`.
10
29
 
11
30
  ### Changed
12
- - First public release on [RubyGems.org](https://rubygems.org/gems/swal_rails). Prior `0.x` tags lived on GitHub Packages only.
31
+ - `flash_map[:alert]` and `flash_map[:error]` now default to a toast
32
+ (top-end, 4s, error icon) instead of a blocking modal. Every built-in
33
+ flash key is a toast out of the box — more consistent and in line with
34
+ how Rails apps typically use `flash[:alert]`. The old modal behavior is
35
+ still opt-in: `config.flash_map[:alert] = { icon: "error", toast: false }`.
36
+ - `default_options` no longer ships with `focusConfirm: true` /
37
+ `returnFocus: true`. Both are SA2's internal defaults already, so
38
+ behavior is unchanged — but listing them explicitly made SA2 warn
39
+ ("incompatible with toasts") on every toast fire. Generator template
40
+ updated to match.
41
+ - Generator initializer comment for `:turbo_override` now mentions
42
+ `Turbo.config.forms.confirm` (Turbo 8.1+) with a fallback to the legacy
43
+ `setConfirmMethod`.
13
44
  - Release workflow now publishes to RubyGems via [Trusted Publishing](https://guides.rubygems.org/trusted-publishing/) (OIDC), no long-lived API key.
45
+ - First public release on [RubyGems.org](https://rubygems.org/gems/swal_rails). Prior `0.x` tags lived on GitHub Packages only.
46
+
47
+ ### Fixed
48
+ - Flash runtime also boots on `turbo:render`, not just `turbo:load`. Form
49
+ submissions that render in place (`render :index, status:
50
+ :unprocessable_entity` for `flash.now` payloads) trigger `turbo:render`
51
+ but not `turbo:load`, so the `swal-flash` meta tag emitted in the new
52
+ body never reached the runtime. The `data-swal-consumed` guard on the
53
+ meta tag dedupes the double-fire on full navigations.
54
+ - Stacked-mode clones render at SA2's standard toast width (360px, capped
55
+ at `calc(100vw - 2rem)`) instead of stretching to the full page. CSS on
56
+ `#swal-rails-stack` mirrors SA2's `body.swal2-toast-shown .swal2-container`
57
+ rules — necessary because the cloned popups live outside SA2's container
58
+ hierarchy.
59
+ - Confirm `:turbo_override` / `:both` writes to `Turbo.config.forms.confirm`
60
+ first (Turbo 8.1+) and falls back to the deprecated
61
+ `Turbo.setConfirmMethod`. Silences the Turbo deprecation warning.
14
62
 
15
63
  ### Security
16
- - `.gitignore` hardened preventively against `.env`, `master.key`, `config/credentials/*.key`, `*.pem`, `*.key`.
17
- - Gemspec now pins `allowed_push_host` to `https://rubygems.org` as a safety net against accidental push to other hosts.
64
+ - `.gitignore` hardened preventively against `.env`, `master.key`,
65
+ `config/credentials/*.key`, `*.pem`, `*.key`.
66
+ - Gemspec pins `allowed_push_host` to `https://rubygems.org` as a safety
67
+ net against accidental push to other hosts.
68
+
69
+ ## [0.3.1.beta2] - 2026-04-24
70
+
71
+ Prerelease snapshot — superseded by [0.3.2].
72
+
73
+ ## [0.3.1.beta1] - 2026-04-21
74
+
75
+ Prerelease snapshot — superseded by [0.3.2].
18
76
 
19
77
  ## [0.3.0] - 2026-04-21
20
78
 
data/README.md CHANGED
@@ -83,7 +83,7 @@ then — it's on **v11** now) and were built for the Rails 5 / UJS era.
83
83
 
84
84
  - 🎨 **SweetAlert2 v11** vendored and pinned — no CDN, no surprise upgrades.
85
85
  - ⚡ **Three asset pipelines**: Importmap (default), jsbundling, Sprockets.
86
- - 🔔 **Auto-wired flash** — `flash[:notice]` toast, `flash[:alert]` → modal, fully mappable per key.
86
+ - 🔔 **Auto-wired flash** — `flash[:notice]` / `flash[:alert]` → toast, stackable, fully mappable per key.
87
87
  - 🛡️ **Turbo confirmations** — replace the native `confirm()` globally **or** opt-in per element.
88
88
  - 🎮 **Stimulus controller** (`data-controller="swal"`) for declarative popups.
89
89
  - 🧱 **Ruby view helpers** — `swal_tag`, `swal_config_meta_tag`, `swal_flash_meta_tag`.
@@ -204,12 +204,16 @@ SwalRails.configure do |config|
204
204
  }
205
205
 
206
206
  # ─── Flash → Swal mapping (per key) ──────────────────────────────────
207
- config.flash_map[:notice] = { icon: "success", toast: true, position: "top-end", timer: 3000 }
208
- config.flash_map[:success] = { icon: "success", toast: true, position: "top-end", timer: 3000 }
209
- config.flash_map[:alert] = { icon: "error", toast: false }
210
- config.flash_map[:error] = { icon: "error", toast: false }
211
- config.flash_map[:warning] = { icon: "warning", toast: true, position: "top-end", timer: 4000 }
212
- config.flash_map[:info] = { icon: "info", toast: true, position: "top-end", timer: 3000 }
207
+ config.flash_map[:notice] = { icon: "success", toast: true, position: "top-end", timer: 3000 }
208
+ config.flash_map[:success] = { icon: "success", toast: true, position: "top-end", timer: 3000 }
209
+ config.flash_map[:alert] = { icon: "error", toast: true, position: "top-end", timer: 4000 }
210
+ config.flash_map[:error] = { icon: "error", toast: true, position: "top-end", timer: 4000 }
211
+ config.flash_map[:warning] = { icon: "warning", toast: true, position: "top-end", timer: 4000 }
212
+ config.flash_map[:info] = { icon: "info", toast: true, position: "top-end", timer: 3000 }
213
+
214
+ # ─── Multi-entry flash playback ─────────────────────────────────────
215
+ config.flash_array_mode = :sequential # :sequential | :stacked
216
+ config.flash_stack_delay = 500 # ms between stacked toasts
213
217
 
214
218
  # ─── I18n scope (for button labels) ──────────────────────────────────
215
219
  config.i18n_scope = "swal_rails"
@@ -228,8 +232,8 @@ end
228
232
  Any flash set from a controller is rendered automatically on page load:
229
233
 
230
234
  ```ruby
231
- flash[:notice] = "Profile updated" # → toast top-right
232
- flash[:alert] = "Could not save" # → modal
235
+ flash[:notice] = "Profile updated" # → success toast top-right
236
+ flash[:alert] = "Could not save" # → error toast top-right (since 0.3.1.beta2)
233
237
  ```
234
238
 
235
239
  Arrays are expanded into one popup per message — handy for model errors:
@@ -248,6 +252,38 @@ flash[:notice] = { text: "Deployed!", icon: "rocket", timer: 5000, toast: true }
248
252
  # → ignores flash_map[:notice], fires a 5-second rocket toast
249
253
  ```
250
254
 
255
+ #### Multi-entry playback: sequential vs stacked
256
+
257
+ When more than one flash entry is set in a single request — either through an
258
+ array of messages under one key, or multiple distinct keys — the runtime
259
+ picks one of two playback modes (configurable via `config.flash_array_mode`):
260
+
261
+ | Mode | Behavior |
262
+ | -------------- | -------- |
263
+ | `:sequential` | **(default)** Each Swal fires only after the previous one closes — chained via Promise callbacks. Predictable but slow on long lists. |
264
+ | `:stacked` | All entries fire in parallel into a fixed top-right container, stacking vertically. Each appears `flash_stack_delay` ms after the previous (default 500ms), then runs its own timer independently. Any `toast: false` entry is forced to toast in this mode. |
265
+
266
+ #### `swal_flash` helper (per-request override)
267
+
268
+ For one-off overrides without touching the global config, use `swal_flash`
269
+ from controllers or views:
270
+
271
+ ```ruby
272
+ # Pile up validation errors as a stack of toasts with a quicker 300ms cadence
273
+ swal_flash :alert, @post.errors.full_messages, mode: :stacked, delay: 300
274
+
275
+ # Same mode but over flash.now (for `render`, not `redirect_to`)
276
+ swal_flash :alert, "Form incomplete", now: true
277
+
278
+ # Extra SA2 options are merged into every entry
279
+ swal_flash :notice, "Deployed!", icon: "rocket", timer: 5000
280
+ ```
281
+
282
+ Signature: `swal_flash(key, messages, mode: nil, delay: nil, now: false, **options)`.
283
+ `mode:` and `delay:` are stored on the flash entry as reserved `_arrayMode` /
284
+ `_stackDelay` meta-keys, extracted by the JS runtime before the options are
285
+ handed to `Swal.fire` — they never leak into SA2.
286
+
251
287
  Behind the scenes, the engine serializes the flash into a meta tag
252
288
  (`<meta name="swal-flash" content="...">`) and the JS runtime reads it and
253
289
  calls `Swal.fire(...)` with your per-key options.
@@ -488,6 +524,8 @@ SwalRails.reset_configuration! # resets to defaults (test fixture he
488
524
  | `expose_window_swal` | Boolean | `true` | When `true`, `window.Swal` is set to the mixed-in `Swal` instance after boot (useful for console debugging and inline scripts). |
489
525
  | `default_options` | Hash | see below | Merged into **every** `Swal.fire(...)` call via `Swal.mixin(...)`. |
490
526
  | `flash_map` | Hash | see below | Flash-key → SA2 options mapping. Keys normalized to symbols. Non-Hash assignment raises `ArgumentError`. |
527
+ | `flash_array_mode` | Symbol | `:sequential` | How multi-entry flash payloads are played: `:sequential` (one at a time, waits for close) or `:stacked` (all in parallel, stacked top-right). Validated. |
528
+ | `flash_stack_delay` | Integer | `500` | Milliseconds between each toast's appearance in `:stacked` mode. |
491
529
  | `i18n_scope` | String | `"swal_rails"` | I18n scope used to look up `confirm_button_text`, `cancel_button_text`, `deny_button_text`, `close_button_aria_label`. Non-string values are coerced. |
492
530
 
493
531
  `confirm_mode` accepted values:
@@ -509,15 +547,18 @@ SwalRails.reset_configuration! # resets to defaults (test fixture he
509
547
 
510
548
  ```ruby
511
549
  {
512
- notice: { icon: "success", toast: true, position: "top-end", timer: 3000, timerProgressBar: true, showConfirmButton: false },
513
- success: { icon: "success", toast: true, position: "top-end", timer: 3000, timerProgressBar: true, showConfirmButton: false },
514
- alert: { icon: "error", toast: false, timer: nil },
515
- error: { icon: "error", toast: false, timer: nil },
516
- warning: { icon: "warning", toast: true, position: "top-end", timer: 4000, timerProgressBar: true, showConfirmButton: false },
517
- info: { icon: "info", toast: true, position: "top-end", timer: 3000, timerProgressBar: true, showConfirmButton: false }
550
+ notice: { icon: "success", toast: true, position: "top-end", timer: 3000, timerProgressBar: true, showConfirmButton: false },
551
+ success: { icon: "success", toast: true, position: "top-end", timer: 3000, timerProgressBar: true, showConfirmButton: false },
552
+ alert: { icon: "error", toast: true, position: "top-end", timer: 4000, timerProgressBar: true, showConfirmButton: false },
553
+ error: { icon: "error", toast: true, position: "top-end", timer: 4000, timerProgressBar: true, showConfirmButton: false },
554
+ warning: { icon: "warning", toast: true, position: "top-end", timer: 4000, timerProgressBar: true, showConfirmButton: false },
555
+ info: { icon: "info", toast: true, position: "top-end", timer: 3000, timerProgressBar: true, showConfirmButton: false }
518
556
  }
519
557
  ```
520
558
 
559
+ > 💡 **Prefer a modal for errors?** Override in your initializer:
560
+ > `config.flash_map[:alert] = { icon: "error", toast: false }`.
561
+
521
562
  #### `to_client_payload` (internal, read-only)
522
563
 
523
564
  Serialization contract consumed by the JS runtime via the
@@ -530,7 +571,9 @@ Serialization contract consumed by the JS runtime via the
530
571
  exposeWindowSwal: Boolean,
531
572
  defaultOptions: Hash,
532
573
  flashMap: Hash,
533
- i18n: Hash # only keys whose translation is present
574
+ flashArrayMode: Symbol, # :sequential | :stacked
575
+ flashStackDelay: Integer, # ms
576
+ i18n: Hash # only keys whose translation is present
534
577
  }
535
578
  ```
536
579
 
@@ -54,10 +54,20 @@ const confirmFlow = (Swal, message, element) => {
54
54
  }
55
55
 
56
56
  const installTurboOverride = (Swal) => {
57
- if (typeof window.Turbo === "undefined" || !window.Turbo.setConfirmMethod) return false
58
-
59
- window.Turbo.setConfirmMethod((message, element) => confirmFlow(Swal, message, element))
60
- return true
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
61
71
  }
62
72
 
63
73
  const installDataAttribute = (Swal) => {
@@ -1,9 +1,119 @@
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.
1
5
  const readFlash = () => {
2
6
  const el = document.querySelector('meta[name="swal-flash"]')
3
- if (!el) return []
7
+ if (!el || el.dataset.swalConsumed === "1") return []
8
+ el.dataset.swalConsumed = "1"
4
9
  try { return JSON.parse(el.getAttribute("content")) || [] } catch { return [] }
5
10
  }
6
11
 
12
+ // Keys attached by `swal_flash` helper for per-request mode / delay override.
13
+ // Stripped from the options before being passed to Swal.fire so they never
14
+ // leak into SA2.
15
+ const META_KEYS = ["_arrayMode", "_stackDelay"]
16
+
17
+ const extractMeta = (queue) => {
18
+ let mode = null
19
+ let delay = null
20
+ for (const item of queue) {
21
+ if (mode === null && item._arrayMode) mode = item._arrayMode
22
+ if (delay === null && item._stackDelay != null) delay = item._stackDelay
23
+ for (const k of META_KEYS) delete item[k]
24
+ }
25
+ return { mode, delay }
26
+ }
27
+
28
+ const STACK_ID = "swal-rails-stack"
29
+
30
+ const ensureStackContainer = () => {
31
+ let el = document.getElementById(STACK_ID)
32
+ if (!el) {
33
+ el = document.createElement("div")
34
+ el.id = STACK_ID
35
+ // 360px matches SA2's `body.swal2-toast-shown .swal2-container` width;
36
+ // without it the cloned popups inherit `width: 100%` from SA2 and
37
+ // visually span the whole screen.
38
+ el.style.cssText = [
39
+ "position:fixed",
40
+ "top:1rem",
41
+ "right:1rem",
42
+ "width:360px",
43
+ "max-width:calc(100vw - 2rem)",
44
+ "display:flex",
45
+ "flex-direction:column",
46
+ "gap:.5rem",
47
+ "z-index:10000",
48
+ "pointer-events:none"
49
+ ].join(";")
50
+ document.body.appendChild(el)
51
+ }
52
+ return el
53
+ }
54
+
55
+ const fireSequential = (Swal, queue) => {
56
+ const fireNext = () => {
57
+ const opts = queue.shift()
58
+ if (!opts) return
59
+ Swal.fire(opts).then(fireNext)
60
+ }
61
+ fireNext()
62
+ }
63
+
64
+ // SA2 is singleton — two concurrent Swal.fire calls collapse into one
65
+ // popup (the second replaces the first). To stack multiple toasts we let
66
+ // SA2 render each popup, clone it into its own slot, then close the
67
+ // original fast so the next fire is unblocked. The clones live on in our
68
+ // stack with their own timer and click-to-dismiss handlers. Empiler des
69
+ // modales bloquantes n'a pas de sens — on force toast: true.
70
+ const fireStacked = async (Swal, queue, delay) => {
71
+ const stack = ensureStackContainer()
72
+ for (let i = 0; i < queue.length; i++) {
73
+ const opts = queue[i]
74
+ const slot = document.createElement("div")
75
+ slot.className = "swal-rails-stack-slot"
76
+ slot.style.cssText = "width:100%;pointer-events:auto;"
77
+ stack.appendChild(slot)
78
+
79
+ const timerMs = opts.timer
80
+ await new Promise((resolve) => {
81
+ Swal.fire({
82
+ ...opts,
83
+ toast: true,
84
+ // Close the SA2 original immediately; the clone in `slot` persists.
85
+ // Animations disabled on the decoy so the clone captures the popup
86
+ // in its normal "shown" state (no opacity-0 from close transition).
87
+ timer: 1,
88
+ timerProgressBar: false,
89
+ showClass: { popup: "", backdrop: "", icon: "" },
90
+ hideClass: { popup: "", backdrop: "", icon: "" },
91
+ didRender: (popup) => {
92
+ const clone = popup.cloneNode(true)
93
+ clone.style.opacity = ""
94
+ clone.querySelectorAll(".swal2-timer-progress-bar-container").forEach((e) => e.remove())
95
+ // SA2 adds `.swal2-icon-show` only after didOpen, but we clone
96
+ // earlier (in didRender) to beat the close animation. Apply it
97
+ // manually so the icon's SVG is visibly drawn in the clone.
98
+ clone.querySelectorAll(".swal2-icon").forEach((icon) => icon.classList.add("swal2-icon-show"))
99
+ slot.appendChild(clone)
100
+ const dismiss = () => {
101
+ if (slot.isConnected) slot.remove()
102
+ if (stack.isConnected && stack.children.length === 0) stack.remove()
103
+ }
104
+ clone.querySelector(".swal2-close")?.addEventListener("click", dismiss)
105
+ if (timerMs) setTimeout(dismiss, timerMs)
106
+ },
107
+ didClose: () => resolve()
108
+ })
109
+ })
110
+
111
+ if (i < queue.length - 1 && delay > 0) {
112
+ await new Promise((r) => setTimeout(r, delay))
113
+ }
114
+ }
115
+ }
116
+
7
117
  export const installFlash = (Swal, config) => {
8
118
  const flashes = readFlash()
9
119
  if (!flashes.length) return
@@ -15,10 +125,14 @@ export const installFlash = (Swal, config) => {
15
125
  return { ...spec, ...(flash.options || {}) }
16
126
  })
17
127
 
18
- const fireNext = () => {
19
- const opts = queue.shift()
20
- if (!opts) return
21
- Swal.fire(opts).then(fireNext)
128
+ const meta = extractMeta(queue)
129
+ const mode = meta.mode || config.flashArrayMode || "sequential"
130
+ const delay = meta.delay != null ? meta.delay
131
+ : (config.flashStackDelay != null ? config.flashStackDelay : 500)
132
+
133
+ if (mode === "stacked" && queue.length > 1) {
134
+ fireStacked(Swal, queue, delay)
135
+ } else {
136
+ fireSequential(Swal, queue)
22
137
  }
23
- fireNext()
24
138
  }
@@ -56,7 +56,14 @@ const ready = (fn) => {
56
56
  }
57
57
 
58
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.
59
65
  document.addEventListener("turbo:load", boot)
66
+ document.addEventListener("turbo:render", boot)
60
67
 
61
68
  export { Swal }
62
69
  export default Swal
@@ -14,12 +14,12 @@ group :development, :test do
14
14
  gem "appraisal", "~> 2.5"
15
15
  gem "capybara", "~> 3.40"
16
16
  gem "cuprite", "~> 0.15"
17
- gem "puma", "~> 6.4"
17
+ gem "puma", "~> 8.0"
18
18
  gem "rspec", "~> 3.12"
19
- gem "rspec-rails", "~> 6.1"
19
+ gem "rspec-rails", "~> 8.0"
20
20
  gem "rubocop", "~> 1.60", require: false
21
21
  gem "rubocop-rspec", require: false
22
- gem "sqlite3", "~> 1.7"
22
+ gem "sqlite3", "~> 2.9"
23
23
  end
24
24
 
25
25
  gemspec path: "../"
@@ -14,12 +14,12 @@ group :development, :test do
14
14
  gem "appraisal", "~> 2.5"
15
15
  gem "capybara", "~> 3.40"
16
16
  gem "cuprite", "~> 0.15"
17
- gem "puma", "~> 6.4"
17
+ gem "puma", "~> 8.0"
18
18
  gem "rspec", "~> 3.12"
19
- gem "rspec-rails", "~> 6.1"
19
+ gem "rspec-rails", "~> 8.0"
20
20
  gem "rubocop", "~> 1.60", require: false
21
21
  gem "rubocop-rspec", require: false
22
- gem "sqlite3", "~> 1.7"
22
+ gem "sqlite3", "~> 2.9"
23
23
  end
24
24
 
25
25
  gemspec path: "../"
@@ -14,12 +14,12 @@ group :development, :test do
14
14
  gem "appraisal", "~> 2.5"
15
15
  gem "capybara", "~> 3.40"
16
16
  gem "cuprite", "~> 0.15"
17
- gem "puma", "~> 6.4"
17
+ gem "puma", "~> 8.0"
18
18
  gem "rspec", "~> 3.12"
19
- gem "rspec-rails", "~> 6.1"
19
+ gem "rspec-rails", "~> 8.0"
20
20
  gem "rubocop", "~> 1.60", require: false
21
21
  gem "rubocop-rspec", require: false
22
- gem "sqlite3", "~> 1.7"
22
+ gem "sqlite3", "~> 2.9"
23
23
  end
24
24
 
25
25
  gemspec path: "../"
@@ -14,12 +14,12 @@ group :development, :test do
14
14
  gem "appraisal", "~> 2.5"
15
15
  gem "capybara", "~> 3.40"
16
16
  gem "cuprite", "~> 0.15"
17
- gem "puma", "~> 6.4"
17
+ gem "puma", "~> 8.0"
18
18
  gem "rspec", "~> 3.12"
19
- gem "rspec-rails", "~> 6.1"
19
+ gem "rspec-rails", "~> 8.0"
20
20
  gem "rubocop", "~> 1.60", require: false
21
21
  gem "rubocop-rspec", require: false
22
- gem "sqlite3", "~> 1.7"
22
+ gem "sqlite3", "~> 2.9"
23
23
  end
24
24
 
25
25
  gemspec path: "../"
@@ -4,7 +4,8 @@ SwalRails.configure do |config|
4
4
  # How confirmation modals are wired.
5
5
  # :off — do nothing, use Swal manually
6
6
  # :data_attribute — intercept clicks/submits on [data-swal-confirm] (default, non-intrusive)
7
- # :turbo_override — replace Turbo.setConfirmMethod globally
7
+ # :turbo_override — replace Turbo.config.forms.confirm globally (Turbo 8.1+),
8
+ # falls back to legacy Turbo.setConfirmMethod on older Turbo
8
9
  # :both — both mechanisms at once
9
10
  config.confirm_mode = :<%= options[:confirm_mode] %>
10
11
 
@@ -15,19 +16,29 @@ SwalRails.configure do |config|
15
16
  config.respect_reduced_motion = true
16
17
 
17
18
  # Default options merged into every Swal.fire call.
19
+ # Note: `focusConfirm` / `returnFocus` are intentionally omitted — SA2
20
+ # already defaults both to `true`, and listing them explicitly makes SA2
21
+ # warn on every toast ("incompatible with toasts").
18
22
  config.default_options = {
19
23
  buttonsStyling: true,
20
- reverseButtons: false,
21
- focusConfirm: true,
22
- returnFocus: true
24
+ reverseButtons: false
23
25
  }
24
26
 
25
27
  # Map Rails flash keys to SweetAlert2 options.
26
28
  # Set a key to nil to silence it. Customize icon/toast/position/timer per key.
27
- config.flash_map[:notice] = { icon: "success", toast: true, position: "top-end", timer: 3000, timerProgressBar: true, showConfirmButton: false }
28
- config.flash_map[:success] = { icon: "success", toast: true, position: "top-end", timer: 3000, timerProgressBar: true, showConfirmButton: false }
29
- config.flash_map[:alert] = { icon: "error", toast: false }
30
- config.flash_map[:error] = { icon: "error", toast: false }
31
- config.flash_map[:warning] = { icon: "warning", toast: true, position: "top-end", timer: 4000, timerProgressBar: true, showConfirmButton: false }
32
- config.flash_map[:info] = { icon: "info", toast: true, position: "top-end", timer: 3000, timerProgressBar: true, showConfirmButton: false }
29
+ config.flash_map[:notice] = { icon: "success", toast: true, position: "top-end", timer: 3000, timerProgressBar: true, showConfirmButton: false }
30
+ config.flash_map[:success] = { icon: "success", toast: true, position: "top-end", timer: 3000, timerProgressBar: true, showConfirmButton: false }
31
+ config.flash_map[:alert] = { icon: "error", toast: true, position: "top-end", timer: 4000, timerProgressBar: true, showConfirmButton: false }
32
+ config.flash_map[:error] = { icon: "error", toast: true, position: "top-end", timer: 4000, timerProgressBar: true, showConfirmButton: false }
33
+ config.flash_map[:warning] = { icon: "warning", toast: true, position: "top-end", timer: 4000, timerProgressBar: true, showConfirmButton: false }
34
+ config.flash_map[:info] = { icon: "info", toast: true, position: "top-end", timer: 3000, timerProgressBar: true, showConfirmButton: false }
35
+
36
+ # How multiple flash entries are played when more than one is set in a
37
+ # single request (or a flash key carries an array of messages).
38
+ # :sequential — one after the other, each waits for the previous to close (default)
39
+ # :stacked — fire all in parallel, stacked vertically in a top-right container,
40
+ # with `flash_stack_delay` ms between each appearance
41
+ # Override per-request with `swal_flash :alert, msgs, mode: :stacked, delay: 300`.
42
+ config.flash_array_mode = :sequential
43
+ config.flash_stack_delay = 500
33
44
  end
@@ -11,24 +11,30 @@ module SwalRails
11
11
  # end
12
12
  class Configuration
13
13
  CONFIRM_MODES = %i[off data_attribute turbo_override both].freeze
14
+ FLASH_ARRAY_MODES = %i[sequential stacked].freeze
14
15
 
15
16
  attr_accessor :default_options,
16
17
  :flash_keys_as_meta,
17
18
  :respect_reduced_motion,
18
- :expose_window_swal
19
- attr_reader :confirm_mode, :flash_map, :i18n_scope
19
+ :expose_window_swal,
20
+ :flash_stack_delay
21
+ attr_reader :confirm_mode, :flash_map, :i18n_scope, :flash_array_mode
20
22
 
21
23
  def initialize
22
24
  @confirm_mode = :data_attribute
23
25
  @flash_keys_as_meta = true
24
26
  @respect_reduced_motion = true
25
27
  @expose_window_swal = true
28
+ @flash_array_mode = :sequential
29
+ @flash_stack_delay = 500
26
30
  @i18n_scope = "swal_rails"
31
+ # `focusConfirm` / `returnFocus` are intentionally omitted: SA2 already
32
+ # defaults both to `true` internally, and passing them explicitly makes
33
+ # SA2 warn on every toast ("incompatible with toasts"). Listing them
34
+ # here would be a no-op behaviorally and a noise generator.
27
35
  @default_options = {
28
36
  buttonsStyling: true,
29
- reverseButtons: false,
30
- focusConfirm: true,
31
- returnFocus: true
37
+ reverseButtons: false
32
38
  }
33
39
  @flash_map = default_flash_map
34
40
  end
@@ -40,6 +46,15 @@ module SwalRails
40
46
  @confirm_mode = value
41
47
  end
42
48
 
49
+ def flash_array_mode=(value)
50
+ value = value.to_sym
51
+ unless FLASH_ARRAY_MODES.include?(value)
52
+ raise ArgumentError, "flash_array_mode must be one of #{FLASH_ARRAY_MODES.inspect}, got #{value.inspect}"
53
+ end
54
+
55
+ @flash_array_mode = value
56
+ end
57
+
43
58
  def i18n_scope=(value)
44
59
  @i18n_scope = value.to_s
45
60
  end
@@ -59,6 +74,8 @@ module SwalRails
59
74
  exposeWindowSwal: expose_window_swal,
60
75
  defaultOptions: default_options,
61
76
  flashMap: flash_map,
77
+ flashArrayMode: flash_array_mode,
78
+ flashStackDelay: flash_stack_delay,
62
79
  i18n: i18n_payload
63
80
  }
64
81
  end
@@ -69,8 +86,8 @@ module SwalRails
69
86
  {
70
87
  notice: { icon: "success", toast: true, position: "top-end", timer: 3000, timerProgressBar: true, showConfirmButton: false },
71
88
  success: { icon: "success", toast: true, position: "top-end", timer: 3000, timerProgressBar: true, showConfirmButton: false },
72
- alert: { icon: "error", toast: false, timer: nil },
73
- error: { icon: "error", toast: false, timer: nil },
89
+ alert: { icon: "error", toast: true, position: "top-end", timer: 4000, timerProgressBar: true, showConfirmButton: false },
90
+ error: { icon: "error", toast: true, position: "top-end", timer: 4000, timerProgressBar: true, showConfirmButton: false },
74
91
  warning: { icon: "warning", toast: true, position: "top-end", timer: 4000, timerProgressBar: true, showConfirmButton: false },
75
92
  info: { icon: "info", toast: true, position: "top-end", timer: 3000, timerProgressBar: true, showConfirmButton: false }
76
93
  }
@@ -40,6 +40,9 @@ module SwalRails
40
40
  initializer "swal_rails.helpers" do
41
41
  ActiveSupport.on_load(:action_controller_base) do
42
42
  helper SwalRails::Helpers
43
+ # Expose `swal_flash` as a controller method so controllers can
44
+ # set flash entries with per-request mode/delay overrides.
45
+ include SwalRails::Helpers
43
46
  end
44
47
  ActiveSupport.on_load(:action_view) do
45
48
  include SwalRails::Helpers
@@ -50,6 +50,48 @@ module SwalRails
50
50
  message.is_a?(Array) ? message : [message]
51
51
  end
52
52
 
53
+ # Controller/view sugar for setting a flash entry with per-request
54
+ # overrides of the global flash_array_mode / flash_stack_delay config.
55
+ #
56
+ # swal_flash :alert, @post.errors.full_messages, mode: :stacked, delay: 300
57
+ # swal_flash :notice, "Deployed!", icon: "rocket", timer: 5000
58
+ # swal_flash :alert, "Oops", now: true # uses flash.now
59
+ #
60
+ # `mode:` — :sequential | :stacked (overrides config.flash_array_mode for this payload)
61
+ # `delay:` — ms between stacked toasts (overrides config.flash_stack_delay)
62
+ # `now:` — write to flash.now (for rendered responses, no redirect)
63
+ # `**options` — any extra SA2 options merged into each entry
64
+ #
65
+ # Meta-keys `_arrayMode` / `_stackDelay` are reserved — the JS runtime
66
+ # strips them before passing options to Swal.fire.
67
+ def swal_flash(key, messages, mode: nil, delay: nil, now: false, **options) # rubocop:disable Metrics/ParameterLists
68
+ entries = build_swal_flash_entries(messages, swal_flash_meta(mode, delay), options)
69
+ return if entries.empty?
70
+
71
+ (now ? flash.now : flash)[key] = entries.size == 1 ? entries.first : entries
72
+ end
73
+
74
+ private
75
+
76
+ def swal_flash_meta(mode, delay)
77
+ meta = {}
78
+ meta[:_arrayMode] = mode.to_s if mode
79
+ meta[:_stackDelay] = Integer(delay) if delay
80
+ meta
81
+ end
82
+
83
+ def build_swal_flash_entries(messages, meta, options)
84
+ list = messages.is_a?(Array) ? messages : [messages]
85
+ list.filter_map do |m|
86
+ next if m.blank?
87
+
88
+ base = m.is_a?(Hash) ? m.symbolize_keys : { text: m.to_s }
89
+ base.merge(options).merge(meta)
90
+ end
91
+ end
92
+
93
+ public
94
+
53
95
  # Generates an inline `<script>` that fires a single Swal.
54
96
  #
55
97
  # Usage: `<%= swal_tag(title: "Hi", icon: "info") %>`
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SwalRails
4
- VERSION = "0.3.1.beta1"
4
+ VERSION = "0.3.2"
5
5
  SWEETALERT2_VERSION = "11.26.24"
6
6
  end
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.1.beta1
4
+ version: 0.3.2
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-04-21 00:00:00.000000000 Z
11
+ date: 2026-04-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: railties