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 +4 -4
- data/CHANGELOG.md +47 -0
- data/README.md +59 -16
- data/app/assets/javascripts/swal_rails/confirm.js +14 -4
- data/app/assets/javascripts/swal_rails/flash.js +120 -6
- data/app/assets/javascripts/swal_rails/index.js +7 -0
- data/lib/generators/swal_rails/install/templates/initializer.rb +19 -9
- data/lib/swal_rails/configuration.rb +24 -7
- data/lib/swal_rails/engine.rb +3 -0
- data/lib/swal_rails/helpers.rb +42 -0
- data/lib/swal_rails/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8bb7a427540f6d06a45d6f18b7a4b29c9771a4890080bbab90dc6806e59502fb
|
|
4
|
+
data.tar.gz: e39bf17dfad033eb1d23b57710ae48f729ebb26da77139ef359a12f3bf88732b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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]`
|
|
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,
|
|
208
|
-
config.flash_map[:success] = { icon: "success", toast: true,
|
|
209
|
-
config.flash_map[:alert] = { icon: "error", toast:
|
|
210
|
-
config.flash_map[:error] = { icon: "error", toast:
|
|
211
|
-
config.flash_map[:warning] = { icon: "warning", toast: true,
|
|
212
|
-
config.flash_map[:info] = { icon: "info", toast: true,
|
|
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" # →
|
|
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,
|
|
513
|
-
success: { icon: "success", toast: true,
|
|
514
|
-
alert: { icon: "error", toast:
|
|
515
|
-
error: { icon: "error", toast:
|
|
516
|
-
warning: { icon: "warning", toast: true,
|
|
517
|
-
info: { icon: "info", toast: true,
|
|
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
|
-
|
|
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"
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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,
|
|
28
|
-
config.flash_map[:success] = { icon: "success", toast: true,
|
|
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,
|
|
32
|
-
config.flash_map[:info] = { icon: "info", toast: true,
|
|
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
|
-
|
|
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:
|
|
73
|
-
error: { icon: "error", toast:
|
|
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
|
}
|
data/lib/swal_rails/engine.rb
CHANGED
|
@@ -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
|
data/lib/swal_rails/helpers.rb
CHANGED
|
@@ -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") %>`
|
data/lib/swal_rails/version.rb
CHANGED
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.
|
|
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-
|
|
11
|
+
date: 2026-04-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: railties
|