swal_rails 0.3.1.beta1 → 0.3.1.beta2

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: 8bb7a427540f6d06a45d6f18b7a4b29c9771a4890080bbab90dc6806e59502fb
4
+ data.tar.gz: e39bf17dfad033eb1d23b57710ae48f729ebb26da77139ef359a12f3bf88732b
5
5
  SHA512:
6
- metadata.gz: 455ee6bc8175ee826535fcfbb7be404f9b56ca2d2a92715f0fe47d818d10521257bcdb8bd640b7b344e26e980aeaa5948d2d237fa097df0aa0e2c534a6fd337a
7
- data.tar.gz: db9ff1404a3e0b33b0e9b70cb04feb9386c2e94b5dad486477d18b319b16d9c64e055eb47a726f491447bcba326010b08f3565de07f343ad3a0033f6634cc925
6
+ metadata.gz: 72f6a4f2df33ecc991599e09e59bcbcb46f9f328b27803c53c0d4d06f3ad6eb665d9fe23170850e52cf1b277ce211cffe6762edf300ca8525225a3fc8d798cc1
7
+ data.tar.gz: 770db1bd51acd17a16e0601d67217bd44428d38fdcff73abcc753192371ce49a5ba451735dd181b476465f90db5de65c6ece483a8ed20d79ec8f30106f427133
data/CHANGELOG.md CHANGED
@@ -6,6 +6,53 @@ 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.beta2] - 2026-04-24
10
+
11
+ ### Fixed
12
+ - Flash runtime now also boots on `turbo:render`, not just `turbo:load`.
13
+ Form submissions that render in place (`render :index, status:
14
+ :unprocessable_entity` for `flash.now` payloads) trigger `turbo:render`
15
+ but not `turbo:load`, so the `swal-flash` meta tag emitted in the new
16
+ body never reached the runtime. The existing `data-swal-consumed`
17
+ guard on the meta tag dedupes the double-fire on full navigations.
18
+ - Stacked-mode clones now render at SA2's standard toast width (360px,
19
+ capped at viewport width minus 2rem) instead of stretching to the full
20
+ page. The fix applies CSS to `#swal-rails-stack` mirroring SA2's
21
+ internal `body.swal2-toast-shown .swal2-container` rules — necessary
22
+ because the cloned popups live outside SA2's container hierarchy.
23
+ - Confirm `:turbo_override` / `:both` now writes to
24
+ `Turbo.config.forms.confirm` first (Turbo 8.1+) and only falls back to
25
+ the deprecated `Turbo.setConfirmMethod`. Silences the Turbo deprecation
26
+ warning logged on every page load.
27
+
28
+ ### Changed
29
+ - `default_options` no longer ships with `focusConfirm: true` /
30
+ `returnFocus: true`. Both are already SA2's internal defaults, so
31
+ behavior is unchanged — but listing them explicitly made SA2 warn
32
+ ("incompatible with toasts") on every toast fire. The generator
33
+ template is updated to match.
34
+ - `flash_map[:alert]` and `flash_map[:error]` now default to a toast
35
+ (top-end, 4s, error icon) instead of a blocking modal. This makes every
36
+ built-in flash key a toast out of the box — more consistent and more in
37
+ line with how Rails apps typically use `flash[:alert]`. Users who want
38
+ the old modal behavior can still opt in:
39
+ `config.flash_map[:alert] = { icon: "error", toast: false }`.
40
+
41
+ ### Added
42
+ - `config.flash_array_mode` (`:sequential` default | `:stacked`) — how a
43
+ multi-entry flash payload is played. Sequential waits for each Swal to
44
+ close before firing the next (current behavior); stacked renders every
45
+ toast in parallel in a fixed top-right container with a configurable
46
+ delay between each appearance.
47
+ - `config.flash_stack_delay` (ms, default 500) — gap between stacked
48
+ toasts in `:stacked` mode.
49
+ - `swal_flash(key, messages, mode:, delay:, now:, **options)` helper,
50
+ available in both controllers and views. Lets a single call override the
51
+ global mode/delay and merge extra SA2 options for the payload:
52
+ `swal_flash :alert, @post.errors.full_messages, mode: :stacked, delay: 300`.
53
+ - Reserved meta-keys `_arrayMode` / `_stackDelay` on flash entry options,
54
+ stripped by the JS runtime before being passed to `Swal.fire`.
55
+
9
56
  ## [0.3.1.beta1] - 2026-04-21
10
57
 
11
58
  ### Changed
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
@@ -15,19 +15,29 @@ SwalRails.configure do |config|
15
15
  config.respect_reduced_motion = true
16
16
 
17
17
  # Default options merged into every Swal.fire call.
18
+ # Note: `focusConfirm` / `returnFocus` are intentionally omitted — SA2
19
+ # already defaults both to `true`, and listing them explicitly makes SA2
20
+ # warn on every toast ("incompatible with toasts").
18
21
  config.default_options = {
19
22
  buttonsStyling: true,
20
- reverseButtons: false,
21
- focusConfirm: true,
22
- returnFocus: true
23
+ reverseButtons: false
23
24
  }
24
25
 
25
26
  # Map Rails flash keys to SweetAlert2 options.
26
27
  # 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 }
28
+ config.flash_map[:notice] = { icon: "success", toast: true, position: "top-end", timer: 3000, timerProgressBar: true, showConfirmButton: false }
29
+ config.flash_map[:success] = { icon: "success", toast: true, position: "top-end", timer: 3000, timerProgressBar: true, showConfirmButton: false }
30
+ config.flash_map[:alert] = { icon: "error", toast: true, position: "top-end", timer: 4000, timerProgressBar: true, showConfirmButton: false }
31
+ config.flash_map[:error] = { icon: "error", toast: true, position: "top-end", timer: 4000, timerProgressBar: true, showConfirmButton: false }
32
+ config.flash_map[:warning] = { icon: "warning", toast: true, position: "top-end", timer: 4000, timerProgressBar: true, showConfirmButton: false }
33
+ config.flash_map[:info] = { icon: "info", toast: true, position: "top-end", timer: 3000, timerProgressBar: true, showConfirmButton: false }
34
+
35
+ # How multiple flash entries are played when more than one is set in a
36
+ # single request (or a flash key carries an array of messages).
37
+ # :sequential — one after the other, each waits for the previous to close (default)
38
+ # :stacked — fire all in parallel, stacked vertically in a top-right container,
39
+ # with `flash_stack_delay` ms between each appearance
40
+ # Override per-request with `swal_flash :alert, msgs, mode: :stacked, delay: 300`.
41
+ config.flash_array_mode = :sequential
42
+ config.flash_stack_delay = 500
33
43
  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.1.beta2"
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.1.beta2
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