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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: db0cf6b419c2e989dfcc877d644fc85244f2c85e0fc2e012661b594854d5cdad
4
- data.tar.gz: f26da33eb89b79b5338eb822281f89d5bc43376ca3a22f39969aaba3c7440f66
3
+ metadata.gz: 9c32d3f037cf95eabb9afc8d578ca88927dbbf95cfca901534f74bc11aad2976
4
+ data.tar.gz: 3e5bef30b1085080e83c98ec3a97b7da2dbd28ea1d2452546ab3252b9fa7f029
5
5
  SHA512:
6
- metadata.gz: 35d5a3a5d411f39870070332ab2516987d9f6345ca3cf17f3fb6b0958f1ae9680608bb1a355ce2ffad3257cabb3e384fee9533018756c90784a29fc176c456c9
7
- data.tar.gz: e48305dfd6fdb0bcb3bab2cbafbf4f56a017006e5d2d95cb716ecff61fa15ffd4abea636206c04e7e57f76a15a7d9888bd64cf6f114ce21bf78a346b52b43424
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
- { title: "Type DELETE to confirm", input: "text" }
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, ...sa2Options } = step || {}
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 { return JSON.parse(el.getAttribute("content")) || [] } catch { return [] }
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
- clone.querySelectorAll(".swal2-timer-progress-bar-container").forEach((e) => e.remove())
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.querySelectorAll(".swal2-icon").forEach((icon) => icon.classList.add("swal2-icon-show"))
99
- slot.appendChild(clone)
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) stack.remove()
103
- }
104
- clone.querySelector(".swal2-close")?.addEventListener("click", dismiss)
105
- if (timerMs) setTimeout(dismiss, timerMs)
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] || map[flash.key.toLowerCase()] || { icon: "info", toast: true, position: "top-end", timer: 3000 }
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 = meta.delay != null ? meta.delay
131
- : (config.flashStackDelay != null ? config.flashStackDelay : 500)
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
+ };
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SwalRails
4
- VERSION = "0.3.3"
4
+ VERSION = "0.3.4"
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.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-04-25 00:00:00.000000000 Z
11
+ date: 2026-05-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: railties