swal_rails 0.3.3 → 0.3.4
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 +19 -0
- data/README.md +12 -1
- data/app/assets/javascripts/swal_rails/chain.js +23 -1
- data/app/assets/javascripts/swal_rails/flash.js +92 -63
- 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: 9c32d3f037cf95eabb9afc8d578ca88927dbbf95cfca901534f74bc11aad2976
|
|
4
|
+
data.tar.gz: 3e5bef30b1085080e83c98ec3a97b7da2dbd28ea1d2452546ab3252b9fa7f029
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 15fc85547a69d5bbadd35cfe7a2f2c8b7f9e7627e75f14161dcdfda5c9ed23595f4ae856716baaea56d8f9ed21f2ff9ded2a9b2ba1a99dd70d51c82b5cf70c62
|
|
7
|
+
data.tar.gz: e7c1aa7c9057d6c4bcc4708259dc38b40096e57b2bc6a870d8dd2bbe0c3cbdf80b19053a75d47649e541524f52b33fbf8df8fa979b64d38920434e068b7a122a
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.3.4] - 2026-05-01
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- Chain step DSL gains `inputExpected` / `inputExpectedError` for typed
|
|
13
|
+
confirmations in JSON-delivered flows (`data-swal-steps`,
|
|
14
|
+
`data-turbo-confirm` arrays, Stimulus `stepsValue`). The runtime injects
|
|
15
|
+
an `inputValidator` that requires exact text (after trim), making "Type
|
|
16
|
+
DELETE" steps enforceable without embedding JavaScript functions.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- Stacked flash toasts no longer render with the icon/title/content
|
|
20
|
+
collapsed into a vertical block. SweetAlert2 only sets
|
|
21
|
+
`popup.style.display = "grid"` at `didOpen`, but the stacked-mode runtime
|
|
22
|
+
clones the popup at `didRender` (one lifecycle step earlier) so the
|
|
23
|
+
inline display is missing. Outside SA2's `.swal2-container` the toast
|
|
24
|
+
fell back to `display: block` and the `.swal2-toast` grid template never
|
|
25
|
+
applied. The clone now pins `display: grid` itself, matching the
|
|
26
|
+
geometry of standard toasts.
|
|
27
|
+
|
|
9
28
|
## [0.3.3] - 2026-04-25
|
|
10
29
|
|
|
11
30
|
### Added
|
data/README.md
CHANGED
|
@@ -363,7 +363,12 @@ server:
|
|
|
363
363
|
swal_steps: [
|
|
364
364
|
{ title: "Delete your account?", icon: "warning" },
|
|
365
365
|
{ title: "This cannot be undone", icon: "error" },
|
|
366
|
-
{
|
|
366
|
+
{
|
|
367
|
+
title: "Type DELETE to confirm",
|
|
368
|
+
input: "text",
|
|
369
|
+
inputExpected: "DELETE",
|
|
370
|
+
inputExpectedError: "Type DELETE exactly"
|
|
371
|
+
}
|
|
367
372
|
].to_json
|
|
368
373
|
} %>
|
|
369
374
|
```
|
|
@@ -373,6 +378,10 @@ icon, buttons, timer, `input:` type, anything SA2 accepts. The per-step
|
|
|
373
378
|
defaults (`showCancelButton: true`, `focusCancel: true`, `icon: "warning"`)
|
|
374
379
|
are merged in first and can be replaced key-by-key.
|
|
375
380
|
|
|
381
|
+
When a step uses `input:`, you can add `inputExpected` (and optional
|
|
382
|
+
`inputExpectedError`) to enforce typed confirmations from JSON-only payloads
|
|
383
|
+
where a function-based `inputValidator` cannot be serialized.
|
|
384
|
+
|
|
376
385
|
#### Conditional branching (`onConfirmed` / `onDenied`)
|
|
377
386
|
|
|
378
387
|
Add a Deny button (`showDenyButton: true`) to get a three-way choice, and
|
|
@@ -717,6 +726,8 @@ Returns `true` iff a complete path through the chain was confirmed. `steps` may
|
|
|
717
726
|
| -------------- | ------------------- | ------ |
|
|
718
727
|
| `onConfirmed` | `Array<StepOptions>` | On `isConfirmed`, run this sub-chain and adopt its boolean result (replaces the remainder of the current chain). |
|
|
719
728
|
| `onDenied` | `Array<StepOptions>` | On `isDenied` (requires `showDenyButton: true`), run this sub-chain and adopt its boolean result. Without this key, `isDenied` aborts the chain. |
|
|
729
|
+
| `inputExpected` | `String` | If present, injects an `inputValidator` that requires an exact match (after trim). Useful for JSON-delivered typed confirmations (e.g. `"DELETE"`). |
|
|
730
|
+
| `inputExpectedError` | `String` | Optional error message used when `inputExpected` does not match. |
|
|
720
731
|
|
|
721
732
|
#### Events
|
|
722
733
|
|
|
@@ -14,6 +14,28 @@ export const CHAIN_DEFAULTS = {
|
|
|
14
14
|
icon: "warning"
|
|
15
15
|
}
|
|
16
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
|
+
|
|
17
39
|
export const chainDialogs = async (Swal, steps) => {
|
|
18
40
|
if (!Array.isArray(steps) || steps.length === 0) return true
|
|
19
41
|
|
|
@@ -21,7 +43,7 @@ export const chainDialogs = async (Swal, steps) => {
|
|
|
21
43
|
// Strip our own control keys — SA2 ignores unknown options, but leaking
|
|
22
44
|
// `onConfirmed`/`onDenied` into the popup options keeps the serialized
|
|
23
45
|
// payload noisy and invites confusion.
|
|
24
|
-
const { onConfirmed, onDenied,
|
|
46
|
+
const { onConfirmed, onDenied, sa2Options } = normalizeStep(step)
|
|
25
47
|
const result = await Swal.fire({ ...CHAIN_DEFAULTS, ...sa2Options })
|
|
26
48
|
|
|
27
49
|
if (result.isDismissed) return false
|
|
@@ -3,35 +3,39 @@
|
|
|
3
3
|
// would have its fireNext chain launched twice, racing and cascading via
|
|
4
4
|
// Swal.fire's replace-current-popup behavior — only the last message wins.
|
|
5
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
|
-
|
|
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
|
+
};
|
|
11
15
|
|
|
12
16
|
// Keys attached by `swal_flash` helper for per-request mode / delay override.
|
|
13
17
|
// Stripped from the options before being passed to Swal.fire so they never
|
|
14
18
|
// leak into SA2.
|
|
15
|
-
const META_KEYS = ["_arrayMode", "_stackDelay"]
|
|
19
|
+
const META_KEYS = ["_arrayMode", "_stackDelay"];
|
|
16
20
|
|
|
17
21
|
const extractMeta = (queue) => {
|
|
18
|
-
let mode = null
|
|
19
|
-
let delay = null
|
|
22
|
+
let mode = null;
|
|
23
|
+
let delay = null;
|
|
20
24
|
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]
|
|
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];
|
|
24
28
|
}
|
|
25
|
-
return { mode, delay }
|
|
26
|
-
}
|
|
29
|
+
return { mode, delay };
|
|
30
|
+
};
|
|
27
31
|
|
|
28
|
-
const STACK_ID = "swal-rails-stack"
|
|
32
|
+
const STACK_ID = "swal-rails-stack";
|
|
29
33
|
|
|
30
34
|
const ensureStackContainer = () => {
|
|
31
|
-
let el = document.getElementById(STACK_ID)
|
|
35
|
+
let el = document.getElementById(STACK_ID);
|
|
32
36
|
if (!el) {
|
|
33
|
-
el = document.createElement("div")
|
|
34
|
-
el.id = STACK_ID
|
|
37
|
+
el = document.createElement("div");
|
|
38
|
+
el.id = STACK_ID;
|
|
35
39
|
// 360px matches SA2's `body.swal2-toast-shown .swal2-container` width;
|
|
36
40
|
// without it the cloned popups inherit `width: 100%` from SA2 and
|
|
37
41
|
// visually span the whole screen.
|
|
@@ -45,21 +49,21 @@ const ensureStackContainer = () => {
|
|
|
45
49
|
"flex-direction:column",
|
|
46
50
|
"gap:.5rem",
|
|
47
51
|
"z-index:10000",
|
|
48
|
-
"pointer-events:none"
|
|
49
|
-
].join(";")
|
|
50
|
-
document.body.appendChild(el)
|
|
52
|
+
"pointer-events:none",
|
|
53
|
+
].join(";");
|
|
54
|
+
document.body.appendChild(el);
|
|
51
55
|
}
|
|
52
|
-
return el
|
|
53
|
-
}
|
|
56
|
+
return el;
|
|
57
|
+
};
|
|
54
58
|
|
|
55
59
|
const fireSequential = (Swal, queue) => {
|
|
56
60
|
const fireNext = () => {
|
|
57
|
-
const opts = queue.shift()
|
|
58
|
-
if (!opts) return
|
|
59
|
-
Swal.fire(opts).then(fireNext)
|
|
60
|
-
}
|
|
61
|
-
fireNext()
|
|
62
|
-
}
|
|
61
|
+
const opts = queue.shift();
|
|
62
|
+
if (!opts) return;
|
|
63
|
+
Swal.fire(opts).then(fireNext);
|
|
64
|
+
};
|
|
65
|
+
fireNext();
|
|
66
|
+
};
|
|
63
67
|
|
|
64
68
|
// SA2 is singleton — two concurrent Swal.fire calls collapse into one
|
|
65
69
|
// popup (the second replaces the first). To stack multiple toasts we let
|
|
@@ -68,15 +72,15 @@ const fireSequential = (Swal, queue) => {
|
|
|
68
72
|
// stack with their own timer and click-to-dismiss handlers. Empiler des
|
|
69
73
|
// modales bloquantes n'a pas de sens — on force toast: true.
|
|
70
74
|
const fireStacked = async (Swal, queue, delay) => {
|
|
71
|
-
const stack = ensureStackContainer()
|
|
75
|
+
const stack = ensureStackContainer();
|
|
72
76
|
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)
|
|
77
|
+
const opts = queue[i];
|
|
78
|
+
const slot = document.createElement("div");
|
|
79
|
+
slot.className = "swal-rails-stack-slot";
|
|
80
|
+
slot.style.cssText = "width:100%;pointer-events:auto;";
|
|
81
|
+
stack.appendChild(slot);
|
|
78
82
|
|
|
79
|
-
const timerMs = opts.timer
|
|
83
|
+
const timerMs = opts.timer;
|
|
80
84
|
await new Promise((resolve) => {
|
|
81
85
|
Swal.fire({
|
|
82
86
|
...opts,
|
|
@@ -89,50 +93,75 @@ const fireStacked = async (Swal, queue, delay) => {
|
|
|
89
93
|
showClass: { popup: "", backdrop: "", icon: "" },
|
|
90
94
|
hideClass: { popup: "", backdrop: "", icon: "" },
|
|
91
95
|
didRender: (popup) => {
|
|
92
|
-
const clone = popup.cloneNode(true)
|
|
93
|
-
clone.style.opacity = ""
|
|
94
|
-
|
|
96
|
+
const clone = popup.cloneNode(true);
|
|
97
|
+
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
|
+
clone
|
|
107
|
+
.querySelectorAll(".swal2-timer-progress-bar-container")
|
|
108
|
+
.forEach((e) => e.remove());
|
|
95
109
|
// SA2 adds `.swal2-icon-show` only after didOpen, but we clone
|
|
96
110
|
// earlier (in didRender) to beat the close animation. Apply it
|
|
97
111
|
// manually so the icon's SVG is visibly drawn in the clone.
|
|
98
|
-
clone
|
|
99
|
-
|
|
112
|
+
clone
|
|
113
|
+
.querySelectorAll(".swal2-icon")
|
|
114
|
+
.forEach((icon) => icon.classList.add("swal2-icon-show"));
|
|
115
|
+
slot.appendChild(clone);
|
|
100
116
|
const dismiss = () => {
|
|
101
|
-
if (slot.isConnected) slot.remove()
|
|
102
|
-
if (stack.isConnected && stack.children.length === 0)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
117
|
+
if (slot.isConnected) slot.remove();
|
|
118
|
+
if (stack.isConnected && stack.children.length === 0)
|
|
119
|
+
stack.remove();
|
|
120
|
+
};
|
|
121
|
+
clone
|
|
122
|
+
.querySelector(".swal2-close")
|
|
123
|
+
?.addEventListener("click", dismiss);
|
|
124
|
+
if (timerMs) setTimeout(dismiss, timerMs);
|
|
106
125
|
},
|
|
107
|
-
didClose: () => resolve()
|
|
108
|
-
})
|
|
109
|
-
})
|
|
126
|
+
didClose: () => resolve(),
|
|
127
|
+
});
|
|
128
|
+
});
|
|
110
129
|
|
|
111
130
|
if (i < queue.length - 1 && delay > 0) {
|
|
112
|
-
await new Promise((r) => setTimeout(r, delay))
|
|
131
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
113
132
|
}
|
|
114
133
|
}
|
|
115
|
-
}
|
|
134
|
+
};
|
|
116
135
|
|
|
117
136
|
export const installFlash = (Swal, config) => {
|
|
118
|
-
const flashes = readFlash()
|
|
119
|
-
if (!flashes.length) return
|
|
137
|
+
const flashes = readFlash();
|
|
138
|
+
if (!flashes.length) return;
|
|
120
139
|
|
|
121
|
-
const map = config.flashMap || {}
|
|
140
|
+
const map = config.flashMap || {};
|
|
122
141
|
const queue = flashes.map((flash) => {
|
|
123
|
-
const spec = map[flash.key] ||
|
|
142
|
+
const spec = map[flash.key] ||
|
|
143
|
+
map[flash.key.toLowerCase()] || {
|
|
144
|
+
icon: "info",
|
|
145
|
+
toast: true,
|
|
146
|
+
position: "top-end",
|
|
147
|
+
timer: 3000,
|
|
148
|
+
};
|
|
124
149
|
// Per-request options win over the per-key defaults from flash_map.
|
|
125
|
-
return { ...spec, ...(flash.options || {}) }
|
|
126
|
-
})
|
|
150
|
+
return { ...spec, ...(flash.options || {}) };
|
|
151
|
+
});
|
|
127
152
|
|
|
128
|
-
const meta = extractMeta(queue)
|
|
129
|
-
const mode = meta.mode || config.flashArrayMode || "sequential"
|
|
130
|
-
const delay =
|
|
131
|
-
|
|
153
|
+
const meta = extractMeta(queue);
|
|
154
|
+
const mode = meta.mode || config.flashArrayMode || "sequential";
|
|
155
|
+
const delay =
|
|
156
|
+
meta.delay != null
|
|
157
|
+
? meta.delay
|
|
158
|
+
: config.flashStackDelay != null
|
|
159
|
+
? config.flashStackDelay
|
|
160
|
+
: 500;
|
|
132
161
|
|
|
133
162
|
if (mode === "stacked" && queue.length > 1) {
|
|
134
|
-
fireStacked(Swal, queue, delay)
|
|
163
|
+
fireStacked(Swal, queue, delay);
|
|
135
164
|
} else {
|
|
136
|
-
fireSequential(Swal, queue)
|
|
165
|
+
fireSequential(Swal, queue);
|
|
137
166
|
}
|
|
138
|
-
}
|
|
167
|
+
};
|
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.
|
|
4
|
+
version: 0.3.4
|
|
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-
|
|
11
|
+
date: 2026-05-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: railties
|