swal_rails 0.3.2 → 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: d9c8d829f1cd2f384c7a40bde0a723bd3a3fd04dccb6fea15b3bd6c543e2a3a7
4
- data.tar.gz: d1b4801ab03a209a14051665b0d410c35971dcc2e0bb17ca16d72e724f446a0f
3
+ metadata.gz: 9c32d3f037cf95eabb9afc8d578ca88927dbbf95cfca901534f74bc11aad2976
4
+ data.tar.gz: 3e5bef30b1085080e83c98ec3a97b7da2dbd28ea1d2452546ab3252b9fa7f029
5
5
  SHA512:
6
- metadata.gz: a72b02157f593c15a9886e2e7c20ebcf6140048cd7f231742096f91215f28cb9273c50b9772405c8d611450d68b89715805f3015393ac15177e28cc981354f22
7
- data.tar.gz: e4590aa66cbd7429b85b5d4a194911a3c1011144f96903293b40eb02ddc72ebb8153da0c0e0bb6fb0b41b327058bfb6a81e81108a66287845af7e4169fcfcf41
6
+ metadata.gz: 15fc85547a69d5bbadd35cfe7a2f2c8b7f9e7627e75f14161dcdfda5c9ed23595f4ae856716baaea56d8f9ed21f2ff9ded2a9b2ba1a99dd70d51c82b5cf70c62
7
+ data.tar.gz: e7c1aa7c9057d6c4bcc4708259dc38b40096e57b2bc6a870d8dd2bbe0c3cbdf80b19053a75d47649e541524f52b33fbf8df8fa979b64d38920434e068b7a122a
data/CHANGELOG.md CHANGED
@@ -6,6 +6,38 @@ 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
+
28
+ ## [0.3.3] - 2026-04-25
29
+
30
+ ### Added
31
+ - Boot-time initializer drift detection. The gem ships
32
+ `SwalRails::INITIALIZER_VERSION`, the install template stamps
33
+ `config.initializer_version = "<current>"`, and a Rails initializer
34
+ hooked after `:load_config_initializers` logs a one-line warning when
35
+ the user's stamp is missing or trails the gem's expected value. Includes
36
+ the regenerate command in the message:
37
+ `bin/rails g swal_rails:install --skip-layout --force`.
38
+ - `config.silence_initializer_warning` (Boolean, default `false`) opt-out
39
+ for users who don't want to regenerate.
40
+
9
41
  ## [0.3.2] - 2026-04-25
10
42
 
11
43
  First stable release on top of the `0.3.1.beta1` + `0.3.1.beta2`
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,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  SwalRails.configure do |config|
4
+ # Stamps this initializer against the template version that shipped with
5
+ # the installed gem. The boot-time check warns if you upgrade the gem to
6
+ # a version with a newer template — regenerate with:
7
+ # bin/rails g swal_rails:install --skip-layout --force
8
+ # Set `config.silence_initializer_warning = true` to silence.
9
+ config.initializer_version = "<%= SwalRails::INITIALIZER_VERSION %>"
10
+
4
11
  # How confirmation modals are wired.
5
12
  # :off — do nothing, use Swal manually
6
13
  # :data_attribute — intercept clicks/submits on [data-swal-confirm] (default, non-intrusive)
@@ -17,7 +17,9 @@ module SwalRails
17
17
  :flash_keys_as_meta,
18
18
  :respect_reduced_motion,
19
19
  :expose_window_swal,
20
- :flash_stack_delay
20
+ :flash_stack_delay,
21
+ :initializer_version,
22
+ :silence_initializer_warning
21
23
  attr_reader :confirm_mode, :flash_map, :i18n_scope, :flash_array_mode
22
24
 
23
25
  def initialize
@@ -28,6 +30,12 @@ module SwalRails
28
30
  @flash_array_mode = :sequential
29
31
  @flash_stack_delay = 500
30
32
  @i18n_scope = "swal_rails"
33
+ # `initializer_version` left nil — apps that haven't regenerated
34
+ # their initializer since `SwalRails::INITIALIZER_VERSION` was
35
+ # introduced (0.3.3) get a one-line warning at boot. Setting it
36
+ # explicitly in the initializer template silences it.
37
+ @initializer_version = nil
38
+ @silence_initializer_warning = false
31
39
  # `focusConfirm` / `returnFocus` are intentionally omitted: SA2 already
32
40
  # defaults both to `true` internally, and passing them explicitly makes
33
41
  # SA2 warn on every toast ("incompatible with toasts"). Listing them
@@ -52,6 +52,13 @@ module SwalRails
52
52
  initializer "swal_rails.i18n" do
53
53
  config.i18n.load_path += Dir[root.join("config/locales/*.yml").to_s]
54
54
  end
55
+
56
+ # Run AFTER user initializers so we can read whatever value they set
57
+ # (or didn't set) for `config.initializer_version`. One-shot, idempotent,
58
+ # opt-out via `config.silence_initializer_warning = true`.
59
+ initializer "swal_rails.check_initializer_version", after: :load_config_initializers do
60
+ SwalRails::InitializerVersionCheck.run!
61
+ end
55
62
  end
56
63
  end
57
64
 
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwalRails
4
+ # Compares the user's `config.initializer_version` against the gem's
5
+ # `SwalRails::INITIALIZER_VERSION` and logs a one-line warning when the
6
+ # initializer is missing the stamp or trails the gem's expected value.
7
+ #
8
+ # Wired into the engine after `:load_config_initializers` so it sees
9
+ # whatever the user set. Silenced via
10
+ # `config.silence_initializer_warning = true`.
11
+ module InitializerVersionCheck
12
+ module_function
13
+
14
+ def run!(logger: default_logger, config: SwalRails.configuration)
15
+ return if config.silence_initializer_warning
16
+
17
+ message = stale_message(config)
18
+ return unless message
19
+
20
+ logger.warn(message)
21
+ end
22
+
23
+ def stale_message(config)
24
+ user = config.initializer_version
25
+ current = SwalRails::INITIALIZER_VERSION
26
+
27
+ return nil if user == current
28
+
29
+ regen = "Run `bin/rails g swal_rails:install --skip-layout --force` to regenerate, or set " \
30
+ "`config.silence_initializer_warning = true` to silence."
31
+
32
+ if user.nil?
33
+ "[swal_rails] config/initializers/swal_rails.rb predates v#{current} " \
34
+ "(no `config.initializer_version` set). New options may be missing. #{regen}"
35
+ else
36
+ "[swal_rails] initializer template advanced to v#{current} (yours: v#{user}). #{regen}"
37
+ end
38
+ end
39
+
40
+ def default_logger
41
+ defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger ? Rails.logger : Logger.new($stderr)
42
+ end
43
+ end
44
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SwalRails
4
- VERSION = "0.3.2"
4
+ VERSION = "0.3.4"
5
5
  SWEETALERT2_VERSION = "11.26.24"
6
6
  end
data/lib/swal_rails.rb CHANGED
@@ -5,6 +5,14 @@ require_relative "swal_rails/version"
5
5
  module SwalRails
6
6
  class Error < StandardError; end
7
7
 
8
+ # The initializer template version this release of the gem ships.
9
+ # Bump only when the template content changes in a way users should
10
+ # know about (new option, removed option, default flip). Compared at
11
+ # boot against `config.initializer_version` to warn about stale
12
+ # config/initializers/swal_rails.rb files. Independent from gem VERSION
13
+ # so non-template-touching releases don't trigger spurious warnings.
14
+ INITIALIZER_VERSION = "0.3.3"
15
+
8
16
  class << self
9
17
  def configuration
10
18
  @configuration ||= Configuration.new
@@ -21,5 +29,6 @@ module SwalRails
21
29
  end
22
30
 
23
31
  require_relative "swal_rails/configuration"
32
+ require_relative "swal_rails/initializer_version_check"
24
33
  require_relative "swal_rails/helpers"
25
34
  require_relative "swal_rails/engine" if defined?(Rails::Engine)
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.2
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
@@ -73,6 +73,7 @@ files:
73
73
  - lib/swal_rails/configuration.rb
74
74
  - lib/swal_rails/engine.rb
75
75
  - lib/swal_rails/helpers.rb
76
+ - lib/swal_rails/initializer_version_check.rb
76
77
  - lib/swal_rails/version.rb
77
78
  - vendor/javascript/sweetalert2/LICENSE
78
79
  - vendor/javascript/sweetalert2/sweetalert2.all.js