swal_rails 0.3.1.beta1

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.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/Appraisals +43 -0
  3. data/CHANGELOG.md +73 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +973 -0
  6. data/Rakefile +12 -0
  7. data/app/assets/javascripts/swal_rails/chain.js +38 -0
  8. data/app/assets/javascripts/swal_rails/confirm.js +93 -0
  9. data/app/assets/javascripts/swal_rails/controllers/swal_controller.js +54 -0
  10. data/app/assets/javascripts/swal_rails/flash.js +24 -0
  11. data/app/assets/javascripts/swal_rails/index.js +62 -0
  12. data/app/assets/stylesheets/swal_rails/index.css +5 -0
  13. data/config/importmap.rb +9 -0
  14. data/config/locales/swal_rails.en.yml +19 -0
  15. data/config/locales/swal_rails.fr.yml +19 -0
  16. data/gemfiles/rails_7_2.gemfile +25 -0
  17. data/gemfiles/rails_8_0.gemfile +25 -0
  18. data/gemfiles/rails_8_1.gemfile +25 -0
  19. data/gemfiles/rails_8_1_sprockets.gemfile +25 -0
  20. data/lib/generators/swal_rails/install/install_generator.rb +138 -0
  21. data/lib/generators/swal_rails/install/templates/initializer.rb +33 -0
  22. data/lib/generators/swal_rails/locales/locales_generator.rb +19 -0
  23. data/lib/swal_rails/configuration.rb +88 -0
  24. data/lib/swal_rails/engine.rb +55 -0
  25. data/lib/swal_rails/helpers.rb +96 -0
  26. data/lib/swal_rails/version.rb +6 -0
  27. data/lib/swal_rails.rb +25 -0
  28. data/vendor/javascript/sweetalert2/LICENSE +22 -0
  29. data/vendor/javascript/sweetalert2/sweetalert2.all.js +4814 -0
  30. data/vendor/javascript/sweetalert2/sweetalert2.all.min.js +6 -0
  31. data/vendor/javascript/sweetalert2/sweetalert2.esm.all.js +4805 -0
  32. data/vendor/javascript/sweetalert2/sweetalert2.esm.all.min.js +6 -0
  33. data/vendor/javascript/sweetalert2/sweetalert2.esm.js +4804 -0
  34. data/vendor/javascript/sweetalert2/sweetalert2.esm.min.js +5 -0
  35. data/vendor/javascript/sweetalert2/sweetalert2.js +4813 -0
  36. data/vendor/javascript/sweetalert2/sweetalert2.min.js +5 -0
  37. data/vendor/stylesheets/sweetalert2/LICENSE +22 -0
  38. data/vendor/stylesheets/sweetalert2/sweetalert2.css +1233 -0
  39. data/vendor/stylesheets/sweetalert2/sweetalert2.min.css +1 -0
  40. data/vendor/stylesheets/sweetalert2/themes/bootstrap-4.css +167 -0
  41. data/vendor/stylesheets/sweetalert2/themes/bootstrap-5.css +173 -0
  42. data/vendor/stylesheets/sweetalert2/themes/borderless.css +46 -0
  43. data/vendor/stylesheets/sweetalert2/themes/bulma.css +94 -0
  44. data/vendor/stylesheets/sweetalert2/themes/material-ui.css +183 -0
  45. data/vendor/stylesheets/sweetalert2/themes/minimal.css +40 -0
  46. metadata +124 -0
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,38 @@
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
+ export const chainDialogs = async (Swal, steps) => {
18
+ if (!Array.isArray(steps) || steps.length === 0) return true
19
+
20
+ for (const step of steps) {
21
+ // Strip our own control keys — SA2 ignores unknown options, but leaking
22
+ // `onConfirmed`/`onDenied` into the popup options keeps the serialized
23
+ // payload noisy and invites confusion.
24
+ const { onConfirmed, onDenied, ...sa2Options } = step || {}
25
+ const result = await Swal.fire({ ...CHAIN_DEFAULTS, ...sa2Options })
26
+
27
+ if (result.isDismissed) return false
28
+ if (result.isConfirmed) {
29
+ if (Array.isArray(onConfirmed)) return chainDialogs(Swal, onConfirmed)
30
+ continue
31
+ }
32
+ if (result.isDenied) {
33
+ if (Array.isArray(onDenied)) return chainDialogs(Swal, onDenied)
34
+ return false
35
+ }
36
+ }
37
+ return true
38
+ }
@@ -0,0 +1,93 @@
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" || !window.Turbo.setConfirmMethod) return false
58
+
59
+ window.Turbo.setConfirmMethod((message, element) => confirmFlow(Swal, message, element))
60
+ return true
61
+ }
62
+
63
+ const installDataAttribute = (Swal) => {
64
+ const handler = (event) => {
65
+ const el = event.target.closest("[data-swal-confirm], [data-swal-steps]")
66
+ if (!el) return
67
+ const message = el.getAttribute("data-swal-confirm")
68
+ event.preventDefault()
69
+ event.stopPropagation()
70
+ confirmFlow(Swal, message, el).then((confirmed) => {
71
+ if (!confirmed) return
72
+ el.removeAttribute("data-swal-confirm")
73
+ el.removeAttribute("data-swal-steps")
74
+ if (typeof el.click === "function" && event.type === "click") {
75
+ el.click()
76
+ } else if (el.tagName === "FORM") {
77
+ // requestSubmit() fires the 'submit' event, so Turbo and any UJS
78
+ // handlers stay in the loop — unlike the raw .submit() which skips them.
79
+ if (typeof el.requestSubmit === "function") el.requestSubmit()
80
+ else el.submit()
81
+ }
82
+ })
83
+ }
84
+ document.addEventListener("click", handler, true)
85
+ document.addEventListener("submit", handler, true)
86
+ }
87
+
88
+ export const installConfirm = (Swal, config) => {
89
+ const mode = config.confirmMode || "data_attribute"
90
+ if (mode === "off") return
91
+ if (mode === "turbo_override" || mode === "both") installTurboOverride(Swal)
92
+ if (mode === "data_attribute" || mode === "both") installDataAttribute(Swal)
93
+ }
@@ -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
+ }
@@ -0,0 +1,24 @@
1
+ const readFlash = () => {
2
+ const el = document.querySelector('meta[name="swal-flash"]')
3
+ if (!el) return []
4
+ try { return JSON.parse(el.getAttribute("content")) || [] } catch { return [] }
5
+ }
6
+
7
+ export const installFlash = (Swal, config) => {
8
+ const flashes = readFlash()
9
+ if (!flashes.length) return
10
+
11
+ const map = config.flashMap || {}
12
+ const queue = flashes.map((flash) => {
13
+ const spec = map[flash.key] || map[flash.key.toLowerCase()] || { icon: "info", toast: true, position: "top-end", timer: 3000 }
14
+ // Per-request options win over the per-key defaults from flash_map.
15
+ return { ...spec, ...(flash.options || {}) }
16
+ })
17
+
18
+ const fireNext = () => {
19
+ const opts = queue.shift()
20
+ if (!opts) return
21
+ Swal.fire(opts).then(fireNext)
22
+ }
23
+ fireNext()
24
+ }
@@ -0,0 +1,62 @@
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
+ document.addEventListener("turbo:load", boot)
60
+
61
+ export { Swal }
62
+ export default Swal
@@ -0,0 +1,5 @@
1
+ /*
2
+ * swal_rails stylesheet entrypoint.
3
+ * Imports the vendored SweetAlert2 CSS; override variables in your own stylesheet.
4
+ */
5
+ @import "sweetalert2.css";
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ pin "sweetalert2", to: "sweetalert2.esm.all.js"
4
+ pin "swal_rails", to: "swal_rails/index.js"
5
+ pin "swal_rails/confirm", to: "swal_rails/confirm.js"
6
+ pin "swal_rails/flash", to: "swal_rails/flash.js"
7
+ pin "swal_rails/chain", to: "swal_rails/chain.js"
8
+ pin_all_from SwalRails::Engine.root.join("app/assets/javascripts/swal_rails/controllers"),
9
+ under: "controllers", to: "swal_rails/controllers"
@@ -0,0 +1,19 @@
1
+ en:
2
+ swal_rails:
3
+ confirm_button_text: "OK"
4
+ cancel_button_text: "Cancel"
5
+ deny_button_text: "No"
6
+ close_button_aria_label: "Close this dialog"
7
+ confirm:
8
+ title: "Are you sure?"
9
+ confirm_button_text: "Yes"
10
+ cancel_button_text: "Cancel"
11
+ flash:
12
+ success:
13
+ title: "Success"
14
+ error:
15
+ title: "Error"
16
+ warning:
17
+ title: "Warning"
18
+ info:
19
+ title: "Info"
@@ -0,0 +1,19 @@
1
+ fr:
2
+ swal_rails:
3
+ confirm_button_text: "OK"
4
+ cancel_button_text: "Annuler"
5
+ deny_button_text: "Non"
6
+ close_button_aria_label: "Fermer cette fenêtre"
7
+ confirm:
8
+ title: "Êtes-vous sûr ?"
9
+ confirm_button_text: "Oui"
10
+ cancel_button_text: "Annuler"
11
+ flash:
12
+ success:
13
+ title: "Succès"
14
+ error:
15
+ title: "Erreur"
16
+ warning:
17
+ title: "Attention"
18
+ info:
19
+ title: "Info"
@@ -0,0 +1,25 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "irb"
6
+ gem "rake", "~> 13.0"
7
+ gem "importmap-rails", "~> 2.0"
8
+ gem "propshaft", "~> 0.9"
9
+ gem "rails", "~> 7.2.2"
10
+ gem "stimulus-rails", "~> 1.3"
11
+ gem "turbo-rails", "~> 2.0"
12
+
13
+ group :development, :test do
14
+ gem "appraisal", "~> 2.5"
15
+ gem "capybara", "~> 3.40"
16
+ gem "cuprite", "~> 0.15"
17
+ gem "puma", "~> 6.4"
18
+ gem "rspec", "~> 3.12"
19
+ gem "rspec-rails", "~> 6.1"
20
+ gem "rubocop", "~> 1.60", require: false
21
+ gem "rubocop-rspec", require: false
22
+ gem "sqlite3", "~> 1.7"
23
+ end
24
+
25
+ gemspec path: "../"
@@ -0,0 +1,25 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "irb"
6
+ gem "rake", "~> 13.0"
7
+ gem "importmap-rails", "~> 2.0"
8
+ gem "propshaft", "~> 1.0"
9
+ gem "rails", "~> 8.0.0"
10
+ gem "stimulus-rails", "~> 1.3"
11
+ gem "turbo-rails", "~> 2.0"
12
+
13
+ group :development, :test do
14
+ gem "appraisal", "~> 2.5"
15
+ gem "capybara", "~> 3.40"
16
+ gem "cuprite", "~> 0.15"
17
+ gem "puma", "~> 6.4"
18
+ gem "rspec", "~> 3.12"
19
+ gem "rspec-rails", "~> 6.1"
20
+ gem "rubocop", "~> 1.60", require: false
21
+ gem "rubocop-rspec", require: false
22
+ gem "sqlite3", "~> 1.7"
23
+ end
24
+
25
+ gemspec path: "../"
@@ -0,0 +1,25 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "irb"
6
+ gem "rake", "~> 13.0"
7
+ gem "importmap-rails", "~> 2.0"
8
+ gem "propshaft", "~> 1.0"
9
+ gem "rails", "~> 8.1.3"
10
+ gem "stimulus-rails", "~> 1.3"
11
+ gem "turbo-rails", "~> 2.0"
12
+
13
+ group :development, :test do
14
+ gem "appraisal", "~> 2.5"
15
+ gem "capybara", "~> 3.40"
16
+ gem "cuprite", "~> 0.15"
17
+ gem "puma", "~> 6.4"
18
+ gem "rspec", "~> 3.12"
19
+ gem "rspec-rails", "~> 6.1"
20
+ gem "rubocop", "~> 1.60", require: false
21
+ gem "rubocop-rspec", require: false
22
+ gem "sqlite3", "~> 1.7"
23
+ end
24
+
25
+ gemspec path: "../"
@@ -0,0 +1,25 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "irb"
6
+ gem "rake", "~> 13.0"
7
+ gem "importmap-rails", "~> 2.0"
8
+ gem "rails", "~> 8.1.3"
9
+ gem "stimulus-rails", "~> 1.3"
10
+ gem "turbo-rails", "~> 2.0"
11
+ gem "sprockets-rails", "~> 3.5"
12
+
13
+ group :development, :test do
14
+ gem "appraisal", "~> 2.5"
15
+ gem "capybara", "~> 3.40"
16
+ gem "cuprite", "~> 0.15"
17
+ gem "puma", "~> 6.4"
18
+ gem "rspec", "~> 3.12"
19
+ gem "rspec-rails", "~> 6.1"
20
+ gem "rubocop", "~> 1.60", require: false
21
+ gem "rubocop-rspec", require: false
22
+ gem "sqlite3", "~> 1.7"
23
+ end
24
+
25
+ gemspec path: "../"
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/base"
5
+
6
+ module SwalRails
7
+ module Generators
8
+ class InstallGenerator < Rails::Generators::Base
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ ASSETS_MODES = %w[importmap jsbundling sprockets auto].freeze
12
+
13
+ # `--mode` is used instead of `--assets` because Rails::Generators::Base
14
+ # reserves `:assets` as a boolean option (legacy `rails new --skip-assets`).
15
+ class_option :mode, type: :string, default: "auto", desc: "Asset mode: importmap, jsbundling, sprockets, auto"
16
+ class_option :confirm_mode, type: :string, default: "data_attribute", desc: "Confirm mode: off, data_attribute, turbo_override, both"
17
+ class_option :skip_layout, type: :boolean, default: false, desc: "Skip layout injection"
18
+
19
+ def validate_options!
20
+ mode = (options[:mode] || "auto").to_s
21
+ return if ASSETS_MODES.include?(mode)
22
+
23
+ raise Thor::Error, "--mode must be one of #{ASSETS_MODES.join(", ")}"
24
+ end
25
+
26
+ def copy_initializer
27
+ template "initializer.rb", "config/initializers/swal_rails.rb"
28
+ end
29
+
30
+ def configure_assets
31
+ case resolved_assets_mode
32
+ when "importmap" then install_importmap
33
+ when "jsbundling" then install_jsbundling
34
+ when "sprockets" then install_sprockets
35
+ end
36
+ end
37
+
38
+ def inject_meta_tags
39
+ return if options[:skip_layout]
40
+
41
+ layout = "app/views/layouts/application.html.erb"
42
+ return say_status(:skip, "#{layout} not found", :yellow) unless file_exists?(layout)
43
+
44
+ inject_into_file layout, before: %r{</head>} do
45
+ " <%= swal_rails_meta_tags %>\n "
46
+ end
47
+ end
48
+
49
+ def show_readme
50
+ readme_text = <<~TXT
51
+
52
+ swal_rails installed.
53
+
54
+ Next steps:
55
+ 1. Edit config/initializers/swal_rails.rb to customize flash_map / confirm_mode.
56
+ 2. Ensure <%= swal_rails_meta_tags %> is rendered in your <head>.
57
+ 3. Import the runtime in your JS entrypoint:
58
+ import "swal_rails"
59
+
60
+ Confirm mode: #{options[:confirm_mode]}
61
+ Assets mode: #{resolved_assets_mode}
62
+ TXT
63
+ say readme_text, :green
64
+ end
65
+
66
+ private
67
+
68
+ def resolved_assets_mode
69
+ @resolved_assets_mode ||= detect_assets_mode
70
+ end
71
+
72
+ def detect_assets_mode
73
+ mode = (options[:mode] || "auto").to_s
74
+ return mode unless mode == "auto"
75
+ return "importmap" if file_exists?("config/importmap.rb")
76
+ return "jsbundling" if file_exists?("package.json")
77
+
78
+ "sprockets"
79
+ end
80
+
81
+ def install_importmap
82
+ pin_line = 'pin "swal_rails", to: "swal_rails/index.js"'
83
+ pin_sa2 = 'pin "sweetalert2", to: "sweetalert2.esm.all.js"'
84
+
85
+ if file_exists?("config/importmap.rb")
86
+ append_unique "config/importmap.rb", pin_sa2
87
+ append_unique "config/importmap.rb", pin_line
88
+ else
89
+ say_status(:warn, "config/importmap.rb not found, skipping pins", :yellow)
90
+ end
91
+
92
+ app_js = "app/javascript/application.js"
93
+ if file_exists?(app_js)
94
+ append_unique app_js, 'import "swal_rails"'
95
+ else
96
+ say_status(:warn, "#{app_js} not found, add `import \"swal_rails\"` to your JS entrypoint", :yellow)
97
+ end
98
+ end
99
+
100
+ def install_jsbundling
101
+ if file_exists?("package.json")
102
+ run "yarn add sweetalert2@#{SwalRails::SWEETALERT2_VERSION}" if file_exists?("yarn.lock")
103
+ run "npm install sweetalert2@#{SwalRails::SWEETALERT2_VERSION}" if file_exists?("package-lock.json") && !file_exists?("yarn.lock")
104
+ else
105
+ say_status(:warn, "package.json not found", :yellow)
106
+ end
107
+
108
+ app_js = "app/javascript/application.js"
109
+ if file_exists?(app_js)
110
+ append_unique app_js, 'import "swal_rails"'
111
+ else
112
+ say_status(:warn, "#{app_js} not found", :yellow)
113
+ end
114
+ end
115
+
116
+ def install_sprockets
117
+ manifest = "app/assets/config/manifest.js"
118
+ if file_exists?(manifest)
119
+ append_unique manifest, "//= link sweetalert2.js"
120
+ append_unique manifest, "//= link sweetalert2.css"
121
+ else
122
+ say_status(:warn, "#{manifest} not found; add `//= require sweetalert2` to your bundle", :yellow)
123
+ end
124
+ end
125
+
126
+ def append_unique(path, line)
127
+ content = File.read(File.join(destination_root, path))
128
+ return if content.include?(line)
129
+
130
+ append_to_file path, "\n#{line}\n"
131
+ end
132
+
133
+ def file_exists?(path)
134
+ File.exist?(File.join(destination_root, path))
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ SwalRails.configure do |config|
4
+ # How confirmation modals are wired.
5
+ # :off — do nothing, use Swal manually
6
+ # :data_attribute — intercept clicks/submits on [data-swal-confirm] (default, non-intrusive)
7
+ # :turbo_override — replace Turbo.setConfirmMethod globally
8
+ # :both — both mechanisms at once
9
+ config.confirm_mode = :<%= options[:confirm_mode] %>
10
+
11
+ # Whether to expose `window.Swal` globally (useful for console / inline scripts).
12
+ config.expose_window_swal = true
13
+
14
+ # Whether to honor the user's OS prefers-reduced-motion setting.
15
+ config.respect_reduced_motion = true
16
+
17
+ # Default options merged into every Swal.fire call.
18
+ config.default_options = {
19
+ buttonsStyling: true,
20
+ reverseButtons: false,
21
+ focusConfirm: true,
22
+ returnFocus: true
23
+ }
24
+
25
+ # Map Rails flash keys to SweetAlert2 options.
26
+ # Set a key to nil to silence it. Customize icon/toast/position/timer per key.
27
+ config.flash_map[:notice] = { icon: "success", toast: true, position: "top-end", timer: 3000, timerProgressBar: true, showConfirmButton: false }
28
+ config.flash_map[:success] = { icon: "success", toast: true, position: "top-end", timer: 3000, timerProgressBar: true, showConfirmButton: false }
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, position: "top-end", timer: 4000, timerProgressBar: true, showConfirmButton: false }
32
+ config.flash_map[:info] = { icon: "info", toast: true, position: "top-end", timer: 3000, timerProgressBar: true, showConfirmButton: false }
33
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+
5
+ module SwalRails
6
+ module Generators
7
+ class LocalesGenerator < Rails::Generators::Base
8
+ source_root File.expand_path("../../../../config/locales", __dir__)
9
+
10
+ desc "Copies swal_rails locale files (en, fr) into config/locales/"
11
+
12
+ def copy_locales
13
+ Dir["#{self.class.source_root}/*.yml"].each do |file|
14
+ copy_file file, "config/locales/#{File.basename(file)}"
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end