swal_rails 0.4.0 → 0.5.0
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 +37 -0
- data/README.md +15 -2
- data/Rakefile +39 -0
- data/app/assets/javascripts/swal_rails/flash.js +29 -22
- data/app/assets/stylesheets/swal_rails/index.css +48 -0
- data/lib/generators/swal_rails/install/install_generator.rb +7 -2
- data/lib/swal_rails/version.rb +1 -1
- data/npm/chain.js +60 -0
- data/npm/confirm.js +103 -0
- data/npm/controllers/swal_controller.js +54 -0
- data/npm/flash.js +174 -0
- data/npm/index.js +69 -0
- data/npm/package.json +42 -0
- metadata +8 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 958168a17777ae58b1ce198ab3de6df9945942a5065c7311201fc05bd4ee8714
|
|
4
|
+
data.tar.gz: 756c79ba6c1bf50bf288a934b807ddda9e09a49eb221097092c72ac3f2981c8b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: aa8b8b60bb0e2c37cd2669d5dbaaa1e14ccbd3c342f97496679b2829294a050be7250ef131bc2d94cb9d469a51daf3f71bd838edc949beefb2040cbb086b13b5
|
|
7
|
+
data.tar.gz: bf4db27a6c234df8aec4e881c99c67578cf575a3c6fdd53092f7f1e827ce01354c2334d677919159cf31a09e2ecc821193cf78f7438409bb89fdcd1e8c71544b
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.5.0] - 2026-05-27
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **npm companion package** (`swal_rails` on npm). The install generator now runs
|
|
13
|
+
`yarn add swal_rails@<version>` (or `npm install`) in jsbundling mode, placing
|
|
14
|
+
the gem's JS runtime in `node_modules/swal_rails` so esbuild / webpack / vite
|
|
15
|
+
can resolve `import "swal_rails"` as a bare specifier — with no bundler
|
|
16
|
+
config required. All sub-paths (`swal_rails/confirm`, `/flash`, `/chain`,
|
|
17
|
+
`/controllers/swal_controller`) are covered via the package `exports` map.
|
|
18
|
+
Closes [#24](https://github.com/Metalzoid/swal_rails/issues/24).
|
|
19
|
+
- **`rake npm:sync`** — copies JS files from `app/assets/javascripts/swal_rails/`
|
|
20
|
+
into `npm/` and bumps `npm/package.json` version to match `SwalRails::VERSION`.
|
|
21
|
+
Run before any npm publish.
|
|
22
|
+
- **`rake npm:publish`** — runs `npm:sync` then `npm publish --access public`.
|
|
23
|
+
- **`_persistent` flash option** — adding `_persistent: true` to a flash entry
|
|
24
|
+
(via `swal_flash :key, msg, _persistent: true` or directly in `flash_map`)
|
|
25
|
+
removes the auto-close timer and forces a visible close button, requiring
|
|
26
|
+
manual dismissal.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- **Stacked flash clone timing** — `didRender` replaced by `willOpen` + `didOpen`
|
|
30
|
+
for cloning SA2 popups into the stack container. The previous `timer: 1` could
|
|
31
|
+
race the `setTimeout(0)` SA2 uses to schedule `didOpen`, leaving slots empty.
|
|
32
|
+
Timer raised to 50 ms; the original popup is hidden via `willOpen` (opacity 0,
|
|
33
|
+
pointer-events none) so the clone is the only visible element.
|
|
34
|
+
- **Stacked toast CSS scope** — SA2 scopes element rules under
|
|
35
|
+
`div:where(.swal2-container)` (zero-specificity `:where()`). Clones rendered
|
|
36
|
+
outside that container lost color, border-radius, and the close-button layout.
|
|
37
|
+
Added explicit `div.swal2-popup`, `div.swal2-html-container`, and
|
|
38
|
+
`button.swal2-close` rules in `index.css` that apply regardless of scope.
|
|
39
|
+
- **`install_jsbundling` generator** — previously only installed `sweetalert2`;
|
|
40
|
+
now also installs `swal_rails` from the npm registry.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
- Release workflow (`.github/workflows/release.yml`) auto-publishes the npm
|
|
44
|
+
package after every gem push via the `NPM_TOKEN` repository secret.
|
|
45
|
+
|
|
9
46
|
## [0.4.0] - 2026-05-03
|
|
10
47
|
|
|
11
48
|
### Added
|
data/README.md
CHANGED
|
@@ -285,6 +285,19 @@ Signature: `swal_flash(key, messages, mode: nil, delay: nil, now: false, **optio
|
|
|
285
285
|
`_stackDelay` meta-keys, extracted by the JS runtime before the options are
|
|
286
286
|
handed to `Swal.fire` — they never leak into SA2.
|
|
287
287
|
|
|
288
|
+
#### Persistent (non-auto-closing) entries
|
|
289
|
+
|
|
290
|
+
Pass `_persistent: true` to any `swal_flash` call — or add it directly to a
|
|
291
|
+
`flash_map` entry — to remove the auto-close timer and force a visible close
|
|
292
|
+
button, requiring the user to dismiss manually:
|
|
293
|
+
|
|
294
|
+
```ruby
|
|
295
|
+
swal_flash :alert, "Your session will expire soon.", _persistent: true
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
The runtime strips `_persistent` before calling `Swal.fire`, then deletes
|
|
299
|
+
`timer` / `timerProgressBar` and sets `showCloseButton: true` on the item.
|
|
300
|
+
|
|
288
301
|
Behind the scenes, the engine serializes the flash into a meta tag
|
|
289
302
|
(`<meta name="swal-flash" content="...">`) and the JS runtime reads it and
|
|
290
303
|
calls `Swal.fire(...)` with your per-key options.
|
|
@@ -765,7 +778,7 @@ Flash entries are `{ key: "notice", options: { text: "..." } }` for string value
|
|
|
765
778
|
Per-mode side effects:
|
|
766
779
|
|
|
767
780
|
- **importmap**: appends `pin "sweetalert2", to: "sweetalert2.esm.all.js"` and `pin "swal_rails", to: "swal_rails/index.js"` to `config/importmap.rb`; appends `import "swal_rails"` to `app/javascript/application.js`.
|
|
768
|
-
- **jsbundling**: runs `yarn add sweetalert2@<pinned>` or `npm install sweetalert2@<pinned>` (based on the lockfile present); appends `import "swal_rails"` to `app/javascript/application.js`.
|
|
781
|
+
- **jsbundling**: runs `yarn add sweetalert2@<pinned> swal_rails@<version>` or `npm install sweetalert2@<pinned> swal_rails@<version>` (based on the lockfile present); appends `import "swal_rails"` to `app/javascript/application.js`. The `swal_rails` npm package ships the same JS runtime as the gem, making the bare specifier `import "swal_rails"` resolvable by any bundler (esbuild, webpack, vite, rollup) without additional configuration.
|
|
769
782
|
- **sprockets**: appends `//= link sweetalert2.js` and `//= link sweetalert2.css` to `app/assets/config/manifest.js`.
|
|
770
783
|
|
|
771
784
|
All append operations are idempotent — running the generator twice is safe.
|
|
@@ -948,7 +961,7 @@ right template automatically.
|
|
|
948
961
|
└──────────────────────────────────────────────────────────────────────┘
|
|
949
962
|
|
|
950
963
|
┌─ jsbundling (esbuild / vite / rollup) ───────────────────────────────┐
|
|
951
|
-
│ package.json → "sweetalert2": "^11"
|
|
964
|
+
│ package.json → "sweetalert2": "^11", "swal_rails": "^0.5" │
|
|
952
965
|
│ app/javascript/application.js │
|
|
953
966
|
│ import "swal_rails" │
|
|
954
967
|
└──────────────────────────────────────────────────────────────────────┘
|
data/Rakefile
CHANGED
|
@@ -10,3 +10,42 @@ require "rubocop/rake_task"
|
|
|
10
10
|
RuboCop::RakeTask.new
|
|
11
11
|
|
|
12
12
|
task default: %i[spec rubocop]
|
|
13
|
+
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
# npm companion package tasks
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
require "fileutils"
|
|
18
|
+
require "json"
|
|
19
|
+
|
|
20
|
+
NPM_JS_SRC = "app/assets/javascripts/swal_rails"
|
|
21
|
+
NPM_JS_DEST = "npm"
|
|
22
|
+
|
|
23
|
+
def npm_sync_files
|
|
24
|
+
require_relative "lib/swal_rails/version"
|
|
25
|
+
|
|
26
|
+
Dir.glob("#{NPM_JS_SRC}/**/*.js").each do |src|
|
|
27
|
+
rel = src.sub("#{NPM_JS_SRC}/", "")
|
|
28
|
+
dest = "#{NPM_JS_DEST}/#{rel}"
|
|
29
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
|
30
|
+
FileUtils.cp(src, dest)
|
|
31
|
+
puts " synced #{rel}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
pkg_path = "#{NPM_JS_DEST}/package.json"
|
|
35
|
+
pkg = JSON.parse(File.read(pkg_path))
|
|
36
|
+
pkg["version"] = SwalRails::VERSION
|
|
37
|
+
File.write(pkg_path, "#{JSON.pretty_generate(pkg)}\n")
|
|
38
|
+
puts " version → #{SwalRails::VERSION}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
namespace :npm do
|
|
42
|
+
desc "Sync JS files from #{NPM_JS_SRC}/ → #{NPM_JS_DEST}/ and bump version in package.json"
|
|
43
|
+
task(:sync) { npm_sync_files }
|
|
44
|
+
|
|
45
|
+
desc "Publish npm package to registry (runs npm:sync first)"
|
|
46
|
+
task publish: :sync do
|
|
47
|
+
Dir.chdir(NPM_JS_DEST) do
|
|
48
|
+
system("npm publish --access public") || abort("npm publish failed")
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -29,6 +29,18 @@ const extractMeta = (queue) => {
|
|
|
29
29
|
return { mode, delay };
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
+
// Per-item: _persistent removes the auto-close timer and ensures the user
|
|
33
|
+
// must dismiss manually via the close button.
|
|
34
|
+
const applyPersistent = (queue) => {
|
|
35
|
+
for (const item of queue) {
|
|
36
|
+
if (!item._persistent) continue;
|
|
37
|
+
delete item._persistent;
|
|
38
|
+
delete item.timer;
|
|
39
|
+
delete item.timerProgressBar;
|
|
40
|
+
item.showCloseButton = true;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
32
44
|
const STACK_ID = "swal-rails-stack";
|
|
33
45
|
|
|
34
46
|
const ensureStackContainer = () => {
|
|
@@ -85,42 +97,36 @@ const fireStacked = async (Swal, queue, delay) => {
|
|
|
85
97
|
Swal.fire({
|
|
86
98
|
...opts,
|
|
87
99
|
toast: true,
|
|
88
|
-
// Close the SA2 original immediately; the clone in `slot` persists.
|
|
89
|
-
// Animations disabled on the decoy so the clone captures the popup
|
|
90
|
-
// in its normal "shown" state (no opacity-0 from close transition).
|
|
91
|
-
timer: 1,
|
|
92
100
|
timerProgressBar: false,
|
|
93
101
|
showClass: { popup: "", backdrop: "", icon: "" },
|
|
94
102
|
hideClass: { popup: "", backdrop: "", icon: "" },
|
|
95
|
-
|
|
103
|
+
// timer:1 races with the setTimeout(0) SA2 uses to schedule didOpen
|
|
104
|
+
// when animations are disabled — didOpen can lose that race and never
|
|
105
|
+
// fire, leaving the slot empty. 50 ms gives the event loop a safe
|
|
106
|
+
// margin while remaining imperceptible to the user.
|
|
107
|
+
timer: 50,
|
|
108
|
+
// Suppress the SA2 original so only our clone in the stack is visible.
|
|
109
|
+
willOpen: (popup) => {
|
|
110
|
+
popup.style.opacity = "0";
|
|
111
|
+
popup.style.pointerEvents = "none";
|
|
112
|
+
},
|
|
113
|
+
// Clone at didOpen: SA2 has applied all inline styles at this point
|
|
114
|
+
// (display:grid, icon classes, close-button grid placement, etc.),
|
|
115
|
+
// so the clone requires no manual fixups.
|
|
116
|
+
didOpen: (popup) => {
|
|
96
117
|
const clone = popup.cloneNode(true);
|
|
118
|
+
// willOpen set opacity:0 on the original; clear it on the clone.
|
|
97
119
|
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
120
|
clone
|
|
107
121
|
.querySelectorAll(".swal2-timer-progress-bar-container")
|
|
108
122
|
.forEach((e) => e.remove());
|
|
109
|
-
// SA2 adds `.swal2-icon-show` only after didOpen, but we clone
|
|
110
|
-
// earlier (in didRender) to beat the close animation. Apply it
|
|
111
|
-
// manually so the icon's SVG is visibly drawn in the clone.
|
|
112
|
-
clone
|
|
113
|
-
.querySelectorAll(".swal2-icon")
|
|
114
|
-
.forEach((icon) => icon.classList.add("swal2-icon-show"));
|
|
115
123
|
slot.appendChild(clone);
|
|
116
124
|
const dismiss = () => {
|
|
117
125
|
if (slot.isConnected) slot.remove();
|
|
118
126
|
if (stack.isConnected && stack.children.length === 0)
|
|
119
127
|
stack.remove();
|
|
120
128
|
};
|
|
121
|
-
clone
|
|
122
|
-
.querySelector(".swal2-close")
|
|
123
|
-
?.addEventListener("click", dismiss);
|
|
129
|
+
clone.querySelector(".swal2-close")?.addEventListener("click", dismiss);
|
|
124
130
|
if (timerMs) setTimeout(dismiss, timerMs);
|
|
125
131
|
},
|
|
126
132
|
didClose: () => resolve(),
|
|
@@ -151,6 +157,7 @@ export const installFlash = (Swal, config) => {
|
|
|
151
157
|
});
|
|
152
158
|
|
|
153
159
|
const meta = extractMeta(queue);
|
|
160
|
+
applyPersistent(queue);
|
|
154
161
|
const mode = meta.mode || config.flashArrayMode || "sequential";
|
|
155
162
|
const delay =
|
|
156
163
|
meta.delay != null
|
|
@@ -3,3 +3,51 @@
|
|
|
3
3
|
* Imports the vendored SweetAlert2 CSS; override variables in your own stylesheet.
|
|
4
4
|
*/
|
|
5
5
|
@import "sweetalert2.css";
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* SA2 scopes all element styles under `div:where(.swal2-container)` for CSS
|
|
9
|
+
* isolation (zero-specificity :where() = easy to override, but only inside the
|
|
10
|
+
* container). This breaks stacked-toast clones that live outside that scope.
|
|
11
|
+
*
|
|
12
|
+
* Fix: re-declare the affected rules without the container ancestor. Our
|
|
13
|
+
* selectors have higher specificity than SA2's :where()-based ones, so they
|
|
14
|
+
* win everywhere — values are identical for native popups, so no regression.
|
|
15
|
+
* Risk of page pollution is near-zero: .swal2-* class names are distinctive.
|
|
16
|
+
*/
|
|
17
|
+
div.swal2-popup {
|
|
18
|
+
color: var(--swal2-color);
|
|
19
|
+
font-size: 1rem;
|
|
20
|
+
border-radius: var(--swal2-border-radius);
|
|
21
|
+
container-name: swal2-popup;
|
|
22
|
+
}
|
|
23
|
+
div.swal2-html-container {
|
|
24
|
+
color: inherit;
|
|
25
|
+
font-weight: normal;
|
|
26
|
+
line-height: normal;
|
|
27
|
+
overflow-wrap: break-word;
|
|
28
|
+
word-break: break-word;
|
|
29
|
+
cursor: initial;
|
|
30
|
+
}
|
|
31
|
+
button.swal2-close {
|
|
32
|
+
z-index: 2;
|
|
33
|
+
align-items: center;
|
|
34
|
+
justify-content: center;
|
|
35
|
+
padding: 0;
|
|
36
|
+
overflow: hidden;
|
|
37
|
+
transition: var(--swal2-close-button-transition);
|
|
38
|
+
border: none;
|
|
39
|
+
border-radius: var(--swal2-border-radius);
|
|
40
|
+
outline: var(--swal2-close-button-outline);
|
|
41
|
+
background: transparent;
|
|
42
|
+
color: var(--swal2-close-button-color);
|
|
43
|
+
font-family: monospace;
|
|
44
|
+
cursor: pointer;
|
|
45
|
+
}
|
|
46
|
+
button.swal2-close:hover {
|
|
47
|
+
background: transparent;
|
|
48
|
+
color: #f27474;
|
|
49
|
+
}
|
|
50
|
+
button.swal2-close:focus-visible {
|
|
51
|
+
outline: none;
|
|
52
|
+
box-shadow: var(--swal2-close-button-focus-box-shadow);
|
|
53
|
+
}
|
|
@@ -99,8 +99,13 @@ module SwalRails
|
|
|
99
99
|
|
|
100
100
|
def install_jsbundling
|
|
101
101
|
if file_exists?("package.json")
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
if file_exists?("yarn.lock")
|
|
103
|
+
run "yarn add sweetalert2@#{SwalRails::SWEETALERT2_VERSION}"
|
|
104
|
+
run "yarn add swal_rails@#{SwalRails::VERSION}"
|
|
105
|
+
elsif file_exists?("package-lock.json")
|
|
106
|
+
run "npm install sweetalert2@#{SwalRails::SWEETALERT2_VERSION}"
|
|
107
|
+
run "npm install swal_rails@#{SwalRails::VERSION}"
|
|
108
|
+
end
|
|
104
109
|
else
|
|
105
110
|
say_status(:warn, "package.json not found", :yellow)
|
|
106
111
|
end
|
data/lib/swal_rails/version.rb
CHANGED
data/npm/chain.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Runs a sequence of SweetAlert2 modals, advancing only on confirm.
|
|
2
|
+
//
|
|
3
|
+
// Semantics (per step):
|
|
4
|
+
// - isDismissed → abort the chain, return false
|
|
5
|
+
// - isConfirmed → run onConfirmed sub-chain if present, else continue
|
|
6
|
+
// - isDenied → run onDenied sub-chain if present, else abort
|
|
7
|
+
//
|
|
8
|
+
// A chain resolves to `true` iff it ran to completion along a path without
|
|
9
|
+
// abort. That boolean is the contract expected by Turbo.setConfirmMethod
|
|
10
|
+
// and by the data-attribute re-dispatch logic in confirm.js.
|
|
11
|
+
export const CHAIN_DEFAULTS = {
|
|
12
|
+
showCancelButton: true,
|
|
13
|
+
focusCancel: true,
|
|
14
|
+
icon: "warning"
|
|
15
|
+
}
|
|
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
|
+
|
|
39
|
+
export const chainDialogs = async (Swal, steps) => {
|
|
40
|
+
if (!Array.isArray(steps) || steps.length === 0) return true
|
|
41
|
+
|
|
42
|
+
for (const step of steps) {
|
|
43
|
+
// Strip our own control keys — SA2 ignores unknown options, but leaking
|
|
44
|
+
// `onConfirmed`/`onDenied` into the popup options keeps the serialized
|
|
45
|
+
// payload noisy and invites confusion.
|
|
46
|
+
const { onConfirmed, onDenied, sa2Options } = normalizeStep(step)
|
|
47
|
+
const result = await Swal.fire({ ...CHAIN_DEFAULTS, ...sa2Options })
|
|
48
|
+
|
|
49
|
+
if (result.isDismissed) return false
|
|
50
|
+
if (result.isConfirmed) {
|
|
51
|
+
if (Array.isArray(onConfirmed)) return chainDialogs(Swal, onConfirmed)
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
if (result.isDenied) {
|
|
55
|
+
if (Array.isArray(onDenied)) return chainDialogs(Swal, onDenied)
|
|
56
|
+
return false
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return true
|
|
60
|
+
}
|
data/npm/confirm.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { chainDialogs } from "swal_rails/chain"
|
|
2
|
+
|
|
3
|
+
const parseJSON = (value) => {
|
|
4
|
+
if (!value) return null
|
|
5
|
+
try { return JSON.parse(value) } catch { return null }
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// When Rails serializes `data: { turbo_confirm: { icon: "error" } }`, the
|
|
9
|
+
// attribute value is a JSON string. Detect that, and accept both Object
|
|
10
|
+
// (single-step options) and Array (multi-step chain) shapes.
|
|
11
|
+
const messagePayload = (message) => {
|
|
12
|
+
if (typeof message !== "string") return null
|
|
13
|
+
const trimmed = message.trim()
|
|
14
|
+
if (trimmed[0] !== "{" && trimmed[0] !== "[") return null
|
|
15
|
+
const parsed = parseJSON(trimmed)
|
|
16
|
+
if (Array.isArray(parsed)) return parsed
|
|
17
|
+
if (parsed && typeof parsed === "object") return parsed
|
|
18
|
+
return null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const confirmDialog = (Swal, message, element) => {
|
|
22
|
+
const dataset = element?.dataset || {}
|
|
23
|
+
const payload = messagePayload(message)
|
|
24
|
+
const fromMessage = payload && !Array.isArray(payload) ? payload : null
|
|
25
|
+
const text = fromMessage ? undefined : message
|
|
26
|
+
|
|
27
|
+
const options = {
|
|
28
|
+
title: dataset.swalTitle || text || "Are you sure?",
|
|
29
|
+
text: dataset.swalText || (dataset.swalTitle ? text : undefined),
|
|
30
|
+
icon: dataset.swalIcon || "warning",
|
|
31
|
+
showCancelButton: true,
|
|
32
|
+
focusCancel: true
|
|
33
|
+
}
|
|
34
|
+
if (dataset.swalConfirmText) options.confirmButtonText = dataset.swalConfirmText
|
|
35
|
+
if (dataset.swalCancelText) options.cancelButtonText = dataset.swalCancelText
|
|
36
|
+
|
|
37
|
+
// Merge order (later wins): defaults → data-swal-* shortcuts → JSON message
|
|
38
|
+
// (turbo_confirm: {}) → data-swal-options (most specific).
|
|
39
|
+
const extras = parseJSON(dataset.swalOptions) || {}
|
|
40
|
+
return Swal.fire({ ...options, ...(fromMessage || {}), ...extras }).then((result) => result.isConfirmed)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Dispatches to either a multi-step chain or a single-step confirm. Called
|
|
44
|
+
// from both the Turbo override and the data-attribute listener so both
|
|
45
|
+
// paths behave identically.
|
|
46
|
+
const confirmFlow = (Swal, message, element) => {
|
|
47
|
+
const fromDataset = parseJSON(element?.dataset?.swalSteps)
|
|
48
|
+
if (Array.isArray(fromDataset) && fromDataset.length) return chainDialogs(Swal, fromDataset)
|
|
49
|
+
|
|
50
|
+
const payload = messagePayload(message)
|
|
51
|
+
if (Array.isArray(payload) && payload.length) return chainDialogs(Swal, payload)
|
|
52
|
+
|
|
53
|
+
return confirmDialog(Swal, message, element)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const installTurboOverride = (Swal) => {
|
|
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
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const installDataAttribute = (Swal) => {
|
|
74
|
+
const handler = (event) => {
|
|
75
|
+
const el = event.target.closest("[data-swal-confirm], [data-swal-steps]")
|
|
76
|
+
if (!el) return
|
|
77
|
+
const message = el.getAttribute("data-swal-confirm")
|
|
78
|
+
event.preventDefault()
|
|
79
|
+
event.stopPropagation()
|
|
80
|
+
confirmFlow(Swal, message, el).then((confirmed) => {
|
|
81
|
+
if (!confirmed) return
|
|
82
|
+
el.removeAttribute("data-swal-confirm")
|
|
83
|
+
el.removeAttribute("data-swal-steps")
|
|
84
|
+
if (typeof el.click === "function" && event.type === "click") {
|
|
85
|
+
el.click()
|
|
86
|
+
} else if (el.tagName === "FORM") {
|
|
87
|
+
// requestSubmit() fires the 'submit' event, so Turbo and any UJS
|
|
88
|
+
// handlers stay in the loop — unlike the raw .submit() which skips them.
|
|
89
|
+
if (typeof el.requestSubmit === "function") el.requestSubmit()
|
|
90
|
+
else el.submit()
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
document.addEventListener("click", handler, true)
|
|
95
|
+
document.addEventListener("submit", handler, true)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const installConfirm = (Swal, config) => {
|
|
99
|
+
const mode = config.confirmMode || "data_attribute"
|
|
100
|
+
if (mode === "off") return
|
|
101
|
+
if (mode === "turbo_override" || mode === "both") installTurboOverride(Swal)
|
|
102
|
+
if (mode === "data_attribute" || mode === "both") installDataAttribute(Swal)
|
|
103
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import Swal from "sweetalert2"
|
|
3
|
+
import { chainDialogs } from "swal_rails/chain"
|
|
4
|
+
|
|
5
|
+
// Fire a Swal modal/toast or a multi-step chain from markup.
|
|
6
|
+
//
|
|
7
|
+
// <button data-controller="swal"
|
|
8
|
+
// data-action="click->swal#fire"
|
|
9
|
+
// data-swal-options-value='{"title":"Hi","icon":"info"}'>
|
|
10
|
+
// Ping
|
|
11
|
+
// </button>
|
|
12
|
+
//
|
|
13
|
+
// <button data-controller="swal"
|
|
14
|
+
// data-action="click->swal#chain"
|
|
15
|
+
// data-swal-steps-value='[{"title":"Sure?"},{"title":"Really?"}]'>
|
|
16
|
+
// Ping
|
|
17
|
+
// </button>
|
|
18
|
+
export default class extends Controller {
|
|
19
|
+
static values = {
|
|
20
|
+
options: { type: Object, default: {} },
|
|
21
|
+
steps: { type: Array, default: [] }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
fire(event) {
|
|
25
|
+
if (this.element.tagName === "A" || this.element.tagName === "BUTTON") {
|
|
26
|
+
event?.preventDefault?.()
|
|
27
|
+
}
|
|
28
|
+
return Swal.fire(this.optionsValue)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
confirm(event) {
|
|
32
|
+
event?.preventDefault?.()
|
|
33
|
+
const form = event?.target?.closest?.("form") || this.element
|
|
34
|
+
Swal.fire({
|
|
35
|
+
showCancelButton: true,
|
|
36
|
+
focusCancel: true,
|
|
37
|
+
...this.optionsValue
|
|
38
|
+
}).then((result) => {
|
|
39
|
+
if (result.isConfirmed && form?.tagName === "FORM") {
|
|
40
|
+
typeof form.requestSubmit === "function" ? form.requestSubmit() : form.submit()
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async chain(event) {
|
|
46
|
+
event?.preventDefault?.()
|
|
47
|
+
const form = event?.target?.closest?.("form") || this.element
|
|
48
|
+
const ok = await chainDialogs(window.Swal || Swal, this.stepsValue)
|
|
49
|
+
if (ok && form?.tagName === "FORM") {
|
|
50
|
+
typeof form.requestSubmit === "function" ? form.requestSubmit() : form.submit()
|
|
51
|
+
}
|
|
52
|
+
return ok
|
|
53
|
+
}
|
|
54
|
+
}
|
data/npm/flash.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
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.
|
|
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
|
+
return JSON.parse(el.getAttribute("content")) || [];
|
|
11
|
+
} catch {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Keys attached by `swal_flash` helper for per-request mode / delay override.
|
|
17
|
+
// Stripped from the options before being passed to Swal.fire so they never
|
|
18
|
+
// leak into SA2.
|
|
19
|
+
const META_KEYS = ["_arrayMode", "_stackDelay"];
|
|
20
|
+
|
|
21
|
+
const extractMeta = (queue) => {
|
|
22
|
+
let mode = null;
|
|
23
|
+
let delay = null;
|
|
24
|
+
for (const item of queue) {
|
|
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];
|
|
28
|
+
}
|
|
29
|
+
return { mode, delay };
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Per-item: _persistent removes the auto-close timer and ensures the user
|
|
33
|
+
// must dismiss manually via the close button.
|
|
34
|
+
const applyPersistent = (queue) => {
|
|
35
|
+
for (const item of queue) {
|
|
36
|
+
if (!item._persistent) continue;
|
|
37
|
+
delete item._persistent;
|
|
38
|
+
delete item.timer;
|
|
39
|
+
delete item.timerProgressBar;
|
|
40
|
+
item.showCloseButton = true;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const STACK_ID = "swal-rails-stack";
|
|
45
|
+
|
|
46
|
+
const ensureStackContainer = () => {
|
|
47
|
+
let el = document.getElementById(STACK_ID);
|
|
48
|
+
if (!el) {
|
|
49
|
+
el = document.createElement("div");
|
|
50
|
+
el.id = STACK_ID;
|
|
51
|
+
// 360px matches SA2's `body.swal2-toast-shown .swal2-container` width;
|
|
52
|
+
// without it the cloned popups inherit `width: 100%` from SA2 and
|
|
53
|
+
// visually span the whole screen.
|
|
54
|
+
el.style.cssText = [
|
|
55
|
+
"position:fixed",
|
|
56
|
+
"top:1rem",
|
|
57
|
+
"right:1rem",
|
|
58
|
+
"width:360px",
|
|
59
|
+
"max-width:calc(100vw - 2rem)",
|
|
60
|
+
"display:flex",
|
|
61
|
+
"flex-direction:column",
|
|
62
|
+
"gap:.5rem",
|
|
63
|
+
"z-index:10000",
|
|
64
|
+
"pointer-events:none",
|
|
65
|
+
].join(";");
|
|
66
|
+
document.body.appendChild(el);
|
|
67
|
+
}
|
|
68
|
+
return el;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const fireSequential = (Swal, queue) => {
|
|
72
|
+
const fireNext = () => {
|
|
73
|
+
const opts = queue.shift();
|
|
74
|
+
if (!opts) return;
|
|
75
|
+
Swal.fire(opts).then(fireNext);
|
|
76
|
+
};
|
|
77
|
+
fireNext();
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// SA2 is singleton — two concurrent Swal.fire calls collapse into one
|
|
81
|
+
// popup (the second replaces the first). To stack multiple toasts we let
|
|
82
|
+
// SA2 render each popup, clone it into its own slot, then close the
|
|
83
|
+
// original fast so the next fire is unblocked. The clones live on in our
|
|
84
|
+
// stack with their own timer and click-to-dismiss handlers. Empiler des
|
|
85
|
+
// modales bloquantes n'a pas de sens — on force toast: true.
|
|
86
|
+
const fireStacked = async (Swal, queue, delay) => {
|
|
87
|
+
const stack = ensureStackContainer();
|
|
88
|
+
for (let i = 0; i < queue.length; i++) {
|
|
89
|
+
const opts = queue[i];
|
|
90
|
+
const slot = document.createElement("div");
|
|
91
|
+
slot.className = "swal-rails-stack-slot";
|
|
92
|
+
slot.style.cssText = "width:100%;pointer-events:auto;";
|
|
93
|
+
stack.appendChild(slot);
|
|
94
|
+
|
|
95
|
+
const timerMs = opts.timer;
|
|
96
|
+
await new Promise((resolve) => {
|
|
97
|
+
Swal.fire({
|
|
98
|
+
...opts,
|
|
99
|
+
toast: true,
|
|
100
|
+
timerProgressBar: false,
|
|
101
|
+
showClass: { popup: "", backdrop: "", icon: "" },
|
|
102
|
+
hideClass: { popup: "", backdrop: "", icon: "" },
|
|
103
|
+
// timer:1 races with the setTimeout(0) SA2 uses to schedule didOpen
|
|
104
|
+
// when animations are disabled — didOpen can lose that race and never
|
|
105
|
+
// fire, leaving the slot empty. 50 ms gives the event loop a safe
|
|
106
|
+
// margin while remaining imperceptible to the user.
|
|
107
|
+
timer: 50,
|
|
108
|
+
// Suppress the SA2 original so only our clone in the stack is visible.
|
|
109
|
+
willOpen: (popup) => {
|
|
110
|
+
popup.style.opacity = "0";
|
|
111
|
+
popup.style.pointerEvents = "none";
|
|
112
|
+
},
|
|
113
|
+
// Clone at didOpen: SA2 has applied all inline styles at this point
|
|
114
|
+
// (display:grid, icon classes, close-button grid placement, etc.),
|
|
115
|
+
// so the clone requires no manual fixups.
|
|
116
|
+
didOpen: (popup) => {
|
|
117
|
+
const clone = popup.cloneNode(true);
|
|
118
|
+
// willOpen set opacity:0 on the original; clear it on the clone.
|
|
119
|
+
clone.style.opacity = "";
|
|
120
|
+
clone
|
|
121
|
+
.querySelectorAll(".swal2-timer-progress-bar-container")
|
|
122
|
+
.forEach((e) => e.remove());
|
|
123
|
+
slot.appendChild(clone);
|
|
124
|
+
const dismiss = () => {
|
|
125
|
+
if (slot.isConnected) slot.remove();
|
|
126
|
+
if (stack.isConnected && stack.children.length === 0)
|
|
127
|
+
stack.remove();
|
|
128
|
+
};
|
|
129
|
+
clone.querySelector(".swal2-close")?.addEventListener("click", dismiss);
|
|
130
|
+
if (timerMs) setTimeout(dismiss, timerMs);
|
|
131
|
+
},
|
|
132
|
+
didClose: () => resolve(),
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (i < queue.length - 1 && delay > 0) {
|
|
137
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export const installFlash = (Swal, config) => {
|
|
143
|
+
const flashes = readFlash();
|
|
144
|
+
if (!flashes.length) return;
|
|
145
|
+
|
|
146
|
+
const map = config.flashMap || {};
|
|
147
|
+
const queue = flashes.map((flash) => {
|
|
148
|
+
const spec = map[flash.key] ||
|
|
149
|
+
map[flash.key.toLowerCase()] || {
|
|
150
|
+
icon: "info",
|
|
151
|
+
toast: true,
|
|
152
|
+
position: "top-end",
|
|
153
|
+
timer: 3000,
|
|
154
|
+
};
|
|
155
|
+
// Per-request options win over the per-key defaults from flash_map.
|
|
156
|
+
return { ...spec, ...(flash.options || {}) };
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const meta = extractMeta(queue);
|
|
160
|
+
applyPersistent(queue);
|
|
161
|
+
const mode = meta.mode || config.flashArrayMode || "sequential";
|
|
162
|
+
const delay =
|
|
163
|
+
meta.delay != null
|
|
164
|
+
? meta.delay
|
|
165
|
+
: config.flashStackDelay != null
|
|
166
|
+
? config.flashStackDelay
|
|
167
|
+
: 500;
|
|
168
|
+
|
|
169
|
+
if (mode === "stacked" && queue.length > 1) {
|
|
170
|
+
fireStacked(Swal, queue, delay);
|
|
171
|
+
} else {
|
|
172
|
+
fireSequential(Swal, queue);
|
|
173
|
+
}
|
|
174
|
+
};
|
data/npm/index.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import Swal from "sweetalert2"
|
|
2
|
+
import { installConfirm } from "swal_rails/confirm"
|
|
3
|
+
import { installFlash } from "swal_rails/flash"
|
|
4
|
+
|
|
5
|
+
const readMeta = (name) => {
|
|
6
|
+
const el = document.querySelector(`meta[name="${name}"]`)
|
|
7
|
+
if (!el) return null
|
|
8
|
+
try { return JSON.parse(el.getAttribute("content")) } catch { return null }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const prefersReducedMotion = () =>
|
|
12
|
+
window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
13
|
+
|
|
14
|
+
const buildMixin = (config) => {
|
|
15
|
+
const base = { ...(config.defaultOptions || {}) }
|
|
16
|
+
if (config.respectReducedMotion && prefersReducedMotion()) {
|
|
17
|
+
base.showClass = { popup: "" }
|
|
18
|
+
base.hideClass = { popup: "" }
|
|
19
|
+
}
|
|
20
|
+
if (config.i18n?.confirm_button_text) base.confirmButtonText = config.i18n.confirm_button_text
|
|
21
|
+
if (config.i18n?.cancel_button_text) base.cancelButtonText = config.i18n.cancel_button_text
|
|
22
|
+
if (config.i18n?.deny_button_text) base.denyButtonText = config.i18n.deny_button_text
|
|
23
|
+
if (config.i18n?.close_button_aria_label) base.closeButtonAriaLabel = config.i18n.close_button_aria_label
|
|
24
|
+
return base
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Module-scoped so repeated calls to boot() (DOMContentLoaded + every
|
|
28
|
+
// turbo:load) don't stack a new click/submit listener per navigation.
|
|
29
|
+
let booted = null
|
|
30
|
+
|
|
31
|
+
const boot = () => {
|
|
32
|
+
if (!booted) {
|
|
33
|
+
const config = readMeta("swal-config") || {}
|
|
34
|
+
const Mixin = Swal.mixin(buildMixin(config))
|
|
35
|
+
|
|
36
|
+
if (config.exposeWindowSwal !== false) {
|
|
37
|
+
window.Swal = Mixin
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
installConfirm(Mixin, config)
|
|
41
|
+
booted = { Swal: Mixin, config }
|
|
42
|
+
document.dispatchEvent(new CustomEvent("swal-rails:ready", { detail: booted }))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Flash meta is re-rendered per request, so read and fire on every page.
|
|
46
|
+
installFlash(booted.Swal, booted.config)
|
|
47
|
+
return booted.Swal
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const ready = (fn) => {
|
|
51
|
+
if (document.readyState === "loading") {
|
|
52
|
+
document.addEventListener("DOMContentLoaded", fn, { once: true })
|
|
53
|
+
} else {
|
|
54
|
+
fn()
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
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.
|
|
65
|
+
document.addEventListener("turbo:load", boot)
|
|
66
|
+
document.addEventListener("turbo:render", boot)
|
|
67
|
+
|
|
68
|
+
export { Swal }
|
|
69
|
+
export default Swal
|
data/npm/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "swal_rails",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "SweetAlert2 for Rails 7+ — batteries included (jsbundling companion)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"module": "index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./index.js",
|
|
10
|
+
"./confirm": "./confirm.js",
|
|
11
|
+
"./flash": "./flash.js",
|
|
12
|
+
"./chain": "./chain.js",
|
|
13
|
+
"./controllers/swal_controller": "./controllers/swal_controller.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"*.js",
|
|
17
|
+
"controllers/"
|
|
18
|
+
],
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"sweetalert2": ">=11.0.0"
|
|
21
|
+
},
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/Metalzoid/swal_rails"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/Metalzoid/swal_rails",
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/Metalzoid/swal_rails/issues"
|
|
29
|
+
},
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"author": "Florian Gagnaire <gagnaire.flo@gmail.com>",
|
|
32
|
+
"keywords": [
|
|
33
|
+
"rails",
|
|
34
|
+
"sweetalert2",
|
|
35
|
+
"flash",
|
|
36
|
+
"confirm",
|
|
37
|
+
"jsbundling",
|
|
38
|
+
"esbuild",
|
|
39
|
+
"webpack",
|
|
40
|
+
"vite"
|
|
41
|
+
]
|
|
42
|
+
}
|
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.
|
|
4
|
+
version: 0.5.0
|
|
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-05-
|
|
11
|
+
date: 2026-05-27 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: railties
|
|
@@ -76,6 +76,12 @@ files:
|
|
|
76
76
|
- lib/swal_rails/helpers.rb
|
|
77
77
|
- lib/swal_rails/initializer_version_check.rb
|
|
78
78
|
- lib/swal_rails/version.rb
|
|
79
|
+
- npm/chain.js
|
|
80
|
+
- npm/confirm.js
|
|
81
|
+
- npm/controllers/swal_controller.js
|
|
82
|
+
- npm/flash.js
|
|
83
|
+
- npm/index.js
|
|
84
|
+
- npm/package.json
|
|
79
85
|
- vendor/javascript/sweetalert2/LICENSE
|
|
80
86
|
- vendor/javascript/sweetalert2/sweetalert2.all.js
|
|
81
87
|
- vendor/javascript/sweetalert2/sweetalert2.all.min.js
|