turbo_overlay 0.3.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.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +436 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +330 -0
  5. data/Rakefile +35 -0
  6. data/app/assets/stylesheets/turbo_overlay.css +234 -0
  7. data/app/javascript/turbo_overlay/dialog_utils.js +46 -0
  8. data/app/javascript/turbo_overlay/hint.js +670 -0
  9. data/app/javascript/turbo_overlay/history.js +184 -0
  10. data/app/javascript/turbo_overlay/index.js +53 -0
  11. data/app/javascript/turbo_overlay/options.js +152 -0
  12. data/app/javascript/turbo_overlay/overlay_controller.js +882 -0
  13. data/app/javascript/turbo_overlay/popover_position.js +64 -0
  14. data/app/javascript/turbo_overlay/setup.js +885 -0
  15. data/app/javascript/turbo_overlay/stack_controller.js +131 -0
  16. data/app/javascript/turbo_overlay/submit_close.js +49 -0
  17. data/app/javascript/turbo_overlay/visit.js +52 -0
  18. data/app/views/layouts/turbo_overlay/drawer.html.erb +5 -0
  19. data/app/views/layouts/turbo_overlay/hint.html.erb +10 -0
  20. data/app/views/layouts/turbo_overlay/modal.html.erb +5 -0
  21. data/app/views/layouts/turbo_overlay/popover.html.erb +5 -0
  22. data/app/views/turbo_overlay/_drawer.html.erb +49 -0
  23. data/app/views/turbo_overlay/_hint.html.erb +6 -0
  24. data/app/views/turbo_overlay/_loading.html.erb +12 -0
  25. data/app/views/turbo_overlay/_modal.html.erb +46 -0
  26. data/app/views/turbo_overlay/_popover.html.erb +54 -0
  27. data/config/importmap.rb +11 -0
  28. data/lib/generators/turbo_overlay/eject_generator.rb +115 -0
  29. data/lib/generators/turbo_overlay/install_generator.rb +443 -0
  30. data/lib/generators/turbo_overlay/templates/chrome/bootstrap3/_confirm.html.erb +13 -0
  31. data/lib/generators/turbo_overlay/templates/chrome/bootstrap3/_drawer.html.erb +50 -0
  32. data/lib/generators/turbo_overlay/templates/chrome/bootstrap3/_hint.html.erb +9 -0
  33. data/lib/generators/turbo_overlay/templates/chrome/bootstrap3/_loading.html.erb +9 -0
  34. data/lib/generators/turbo_overlay/templates/chrome/bootstrap3/_modal.html.erb +49 -0
  35. data/lib/generators/turbo_overlay/templates/chrome/bootstrap3/_popover.html.erb +54 -0
  36. data/lib/generators/turbo_overlay/templates/chrome/bootstrap5/_confirm.html.erb +13 -0
  37. data/lib/generators/turbo_overlay/templates/chrome/bootstrap5/_drawer.html.erb +55 -0
  38. data/lib/generators/turbo_overlay/templates/chrome/bootstrap5/_hint.html.erb +9 -0
  39. data/lib/generators/turbo_overlay/templates/chrome/bootstrap5/_loading.html.erb +9 -0
  40. data/lib/generators/turbo_overlay/templates/chrome/bootstrap5/_modal.html.erb +58 -0
  41. data/lib/generators/turbo_overlay/templates/chrome/bootstrap5/_popover.html.erb +53 -0
  42. data/lib/generators/turbo_overlay/templates/chrome/plain/_confirm.html.erb +14 -0
  43. data/lib/generators/turbo_overlay/templates/chrome/tailwind/_confirm.html.erb +17 -0
  44. data/lib/generators/turbo_overlay/templates/chrome/tailwind/_drawer.html.erb +55 -0
  45. data/lib/generators/turbo_overlay/templates/chrome/tailwind/_hint.html.erb +6 -0
  46. data/lib/generators/turbo_overlay/templates/chrome/tailwind/_loading.html.erb +9 -0
  47. data/lib/generators/turbo_overlay/templates/chrome/tailwind/_modal.html.erb +46 -0
  48. data/lib/generators/turbo_overlay/templates/chrome/tailwind/_popover.html.erb +54 -0
  49. data/lib/generators/turbo_overlay/templates/initializer.rb.tt +67 -0
  50. data/lib/turbo_overlay/configuration.rb +226 -0
  51. data/lib/turbo_overlay/controller.rb +405 -0
  52. data/lib/turbo_overlay/engine.rb +52 -0
  53. data/lib/turbo_overlay/helpers/stream_helper.rb +77 -0
  54. data/lib/turbo_overlay/helpers/view_helper.rb +651 -0
  55. data/lib/turbo_overlay/version.rb +3 -0
  56. data/lib/turbo_overlay.rb +20 -0
  57. metadata +161 -0
@@ -0,0 +1,131 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import { getPopoverTrigger, clearPopoverTrigger } from "turbo_overlay/setup"
3
+ import { setStackController } from "turbo_overlay/history"
4
+
5
+ // Per-page stack registry. Mounted once on the host page via
6
+ // `<%= overlay_stack_tag %>` (DOM id `turbo_overlay_stack`). Tracks the
7
+ // order of currently-open overlays and routes server-issued
8
+ // `turbo_stream.overlay(:close, …)` events to the matching overlay's
9
+ // controller.
10
+ //
11
+ // Cross-cutting wiring (request-header injection, loading
12
+ // placeholders, back/forward cache teardown, themed confirm) lives in
13
+ // `turbo_overlay/setup`, which self-bootstraps on import from
14
+ // `turbo_overlay` (the gem's entry point).
15
+
16
+ export default class extends Controller {
17
+ static values = {
18
+ allowedClickOutsideSelectors: { type: Array, default: [] }
19
+ }
20
+
21
+ connect() {
22
+ this.entries = []
23
+
24
+ this._closeHandler = (event) => this.handleCloseEvent(event)
25
+ window.addEventListener("turbo-overlay:close", this._closeHandler)
26
+
27
+ setStackController(this)
28
+ }
29
+
30
+ disconnect() {
31
+ window.removeEventListener("turbo-overlay:close", this._closeHandler)
32
+ this.entries = []
33
+ setStackController(null)
34
+ }
35
+
36
+ register(entry) {
37
+ if (this.entries.some((e) => e.id === entry.id)) return false
38
+
39
+ // Single-popover behavior: opening a new popover closes any other
40
+ // open popovers. Modals and drawers keep their existing stacking.
41
+ if (entry.type === "popover") {
42
+ const existing = this.entries.filter((e) => e.type === "popover")
43
+ for (const prior of existing) {
44
+ if (prior.controller && typeof prior.controller.close === "function") {
45
+ prior.controller.close()
46
+ }
47
+ }
48
+ }
49
+
50
+ this.entries.push(entry)
51
+ return true
52
+ }
53
+
54
+ unregister(id) {
55
+ const before = this.entries.length
56
+ this.entries = this.entries.filter((e) => e.id !== id)
57
+ // Only clear the popover-trigger registry when we actually removed
58
+ // the entry. The disconnect path schedules a deferred unregister
59
+ // via queueMicrotask as a safety net for elements ripped out of
60
+ // the DOM without a close() — if close() already ran (and cleared
61
+ // the entry), that deferred call must not clobber a popoverTriggers
62
+ // entry that meanwhile has been re-pointed at a new overlay
63
+ // reusing the same id (e.g. a re-click on the same popover_link_to).
64
+ if (this.entries.length < before) clearPopoverTrigger(id)
65
+ }
66
+
67
+ getPopoverTrigger(id) {
68
+ return getPopoverTrigger(id)
69
+ }
70
+
71
+ has(id) {
72
+ return this.entries.some((e) => e.id === id)
73
+ }
74
+
75
+ updateController(id, controller) {
76
+ const entry = this.entries.find((e) => e.id === id)
77
+ if (entry) entry.controller = controller
78
+ }
79
+
80
+ get depth() {
81
+ return this.entries.length
82
+ }
83
+
84
+ topEntry(typeFilter = null) {
85
+ for (let i = this.entries.length - 1; i >= 0; i--) {
86
+ if (!typeFilter || this.entries[i].type === typeFilter) return this.entries[i]
87
+ }
88
+ return null
89
+ }
90
+
91
+ handleCloseEvent(event) {
92
+ const detail = event.detail || {}
93
+ const scope = detail.scope || "top"
94
+ const type = detail.type || null
95
+ const id = detail.id || null
96
+
97
+ const closes = []
98
+ if (id) {
99
+ const entry = this.entries.find((e) => e.id === id)
100
+ if (entry && entry.controller && typeof entry.controller.close === "function") {
101
+ closes.push(entry.controller.close())
102
+ }
103
+ } else if (scope === "all") {
104
+ const targets = type
105
+ ? this.entries.filter((e) => e.type === type)
106
+ : this.entries.slice()
107
+ for (let i = targets.length - 1; i >= 0; i--) {
108
+ const c = targets[i].controller
109
+ if (c && typeof c.close === "function") closes.push(c.close())
110
+ }
111
+ } else {
112
+ const top = this.topEntry(type)
113
+ if (top && top.controller && typeof top.controller.close === "function") {
114
+ closes.push(top.controller.close())
115
+ }
116
+ }
117
+
118
+ // Server-requested post-close navigation. Await the close
119
+ // animation(s) so the new page doesn't paint behind a
120
+ // still-animating overlay. The visit defaults to "advance" — pass
121
+ // visit_action: :replace from the server when you want the
122
+ // current history entry rewritten (e.g., a stale URL the user
123
+ // shouldn't be able to back into).
124
+ if (detail.visit && typeof window !== "undefined" && window.Turbo &&
125
+ typeof window.Turbo.visit === "function") {
126
+ Promise.all(closes).then(() => {
127
+ window.Turbo.visit(detail.visit, { action: detail.visitAction || "advance" })
128
+ })
129
+ }
130
+ }
131
+ }
@@ -0,0 +1,49 @@
1
+ // Pure decision function for the per-dialog `turbo:submit-end`
2
+ // listener installed by the overlay controller. Returns true when
3
+ // the overlay should close in response to a form submission's redirect.
4
+ //
5
+ // Inputs:
6
+ // form — the submitting <form> element (event.target)
7
+ // dialog — the overlay <dialog> hosting the controller
8
+ // fetchResponse — event.detail.fetchResponse from turbo:submit-end
9
+ //
10
+ // Close-on-redirect is the default. Three opt-outs, finest-grained
11
+ // wins. None of these short-circuit the others — checking the form
12
+ // attribute first means a form-level opt-out works inside an overlay
13
+ // whose link did not opt out, and the dialog attribute is meaningless
14
+ // for forms outside the dialog (filtered by descendant check first).
15
+ //
16
+ // 1. Form is not a descendant of this dialog — not our submission.
17
+ // 2. Response is not a followed redirect — leave it alone.
18
+ // 3. The form opts out via data-turbo-overlay-keep-open-on-redirect.
19
+ // 4. The dialog opts out (link helper set keep_overlay_open_on_redirect: true).
20
+ export function shouldCloseOnRedirect({ form, dialog, fetchResponse }) {
21
+ if (!form || !dialog) return false
22
+ if (!form.tagName || form.tagName !== "FORM") return false
23
+ if (!dialog.contains(form)) return false
24
+ if (!fetchResponse || !fetchResponse.redirected) return false
25
+ if (form.dataset && form.dataset.turboOverlayKeepOpenOnRedirect === "true") return false
26
+ if (dialog.dataset && dialog.dataset.turboOverlayKeepOpenOnRedirect === "true") return false
27
+ return true
28
+ }
29
+
30
+ // Same-page-redirect predicate for the smooth-close path: returns true
31
+ // when the redirect target's pathname matches the URL the overlay was
32
+ // opened from. Used by overlay_controller to decide between the
33
+ // morph-behind path (true) and the existing close-then-visit path
34
+ // (false). Pathname-only — query string and hash differ freely.
35
+ // Cross-origin and missing inputs return false.
36
+ export function isSamePageRedirect({ dialog, fetchResponse }) {
37
+ const openerUrl = dialog && dialog.dataset && dialog.dataset.turboOverlayOpenerUrl
38
+ if (!openerUrl) return false
39
+ const respUrl = fetchResponse && fetchResponse.response && fetchResponse.response.url
40
+ if (!respUrl) return false
41
+ try {
42
+ const base = (typeof document !== "undefined" && document.baseURI) || undefined
43
+ const a = new URL(openerUrl, base)
44
+ const b = new URL(respUrl, base)
45
+ return a.origin === b.origin && a.pathname === b.pathname
46
+ } catch (_) {
47
+ return false
48
+ }
49
+ }
@@ -0,0 +1,52 @@
1
+ import { buildOverlayDataset, validateVisitArgs } from "turbo_overlay/options"
2
+
3
+ // Open an overlay from JavaScript — the programmatic counterpart to
4
+ // `modal_link_to` / `drawer_link_to` / `popover_link_to`. Reuses the
5
+ // existing click pipeline: a detached `<a>` is built with the right
6
+ // `data-turbo-overlay-*` attributes, briefly attached to the document,
7
+ // clicked, and removed. Turbo's FormLinkClickObserver routes the click
8
+ // through its stream-fetch path, the gem's document click hook captures
9
+ // the trigger, and `turbo:before-fetch-request` emits the X-Turbo-Overlay-*
10
+ // headers as it does for any link click.
11
+ //
12
+ // Use this for non-anchor triggers (map pins, canvas hit-tests, custom
13
+ // elements). For ordinary HTML links, prefer the Rails helpers.
14
+ //
15
+ // TurboOverlay.visit("/places/123") // modal (default)
16
+ // TurboOverlay.visit("/cart", { type: "drawer", advance: true })
17
+ // TurboOverlay.visit("/preview/9", { type: "popover", anchor: pinEl, position: "top" })
18
+ //
19
+ // Popovers must supply an `anchor` element — positioning derives from
20
+ // its `getBoundingClientRect()`. The synthesized link has no useful rect
21
+ // of its own.
22
+ //
23
+ // Validation, type list, and dataset construction live in `options.js`
24
+ // so they round-trip with the same table the fetch hook reads and stay
25
+ // unit-testable without a DOM.
26
+
27
+ export function visit(url, options = {}) {
28
+ const type = validateVisitArgs(url, options)
29
+ const link = document.createElement("a")
30
+ link.href = url
31
+ link.hidden = true
32
+ link.style.position = "absolute"
33
+ link.style.left = "-9999px"
34
+ const dataset = buildOverlayDataset(type, options)
35
+ for (const k in dataset) link.dataset[k] = dataset[k]
36
+ if (options.anchor) {
37
+ // Non-enumerable so it doesn't show up via spread/JSON, but the
38
+ // click handler in setup.js can still read it.
39
+ Object.defineProperty(link, "__turboOverlayAnchor", {
40
+ value: options.anchor,
41
+ writable: false,
42
+ enumerable: false,
43
+ configurable: true,
44
+ })
45
+ }
46
+ document.body.appendChild(link)
47
+ try {
48
+ link.click()
49
+ } finally {
50
+ link.remove()
51
+ }
52
+ }
@@ -0,0 +1,5 @@
1
+ <%= overlay_response_wrapper(:drawer) do %>
2
+ <%= render layout: "turbo_overlay/drawer" do %>
3
+ <%= yield %>
4
+ <% end %>
5
+ <% end %>
@@ -0,0 +1,10 @@
1
+ <%# Layout for :hint-variant responses fetched by the gem's hint
2
+ module via explicit `hint_url:`. Same wire shape as the
3
+ inline-hint template emitted by `overlay_stack_tag` so the JS
4
+ extractor has one code path: a <template id="…"> wrapping the
5
+ hint chrome partial wrapping the response body.
6
+
7
+ The body is whatever the action's view rendered — typically a
8
+ `show.html+hint.erb` variant template, but a plain
9
+ `show.html.erb` works if the entire view fits a hint preview. %>
10
+ <template id="turbo-overlay-hint"><%= render layout: "turbo_overlay/hint" do %><%= yield %><% end %></template>
@@ -0,0 +1,5 @@
1
+ <%= overlay_response_wrapper(:modal) do %>
2
+ <%= render layout: "turbo_overlay/modal" do %>
3
+ <%= yield %>
4
+ <% end %>
5
+ <% end %>
@@ -0,0 +1,5 @@
1
+ <%= overlay_response_wrapper(:popover) do %>
2
+ <%= render layout: "turbo_overlay/popover" do %>
3
+ <%= yield %>
4
+ <% end %>
5
+ <% end %>
@@ -0,0 +1,49 @@
1
+ <% loading = local_assigns.fetch(:loading, false) %>
2
+ <% position = turbo_overlay_position || TurboOverlay.configuration.drawer.position %>
3
+ <% backdrop = turbo_overlay_backdrop? %>
4
+ <% close_button = local_assigns.fetch(:close) { overlay_close? } %>
5
+ <dialog class="<%= class_names("turbo-overlay", "turbo-overlay--drawer", "turbo-overlay--drawer-#{position}", "turbo-drawer", "turbo-overlay--loading": loading, "turbo-overlay--no-backdrop": !backdrop) %>"
6
+ <% if loading %>
7
+ role="status"
8
+ aria-live="polite"
9
+ aria-label="Loading"
10
+ <% else %>
11
+ data-controller="turbo-overlay"
12
+ data-turbo-overlay-id-value="<%= turbo_overlay_id %>"
13
+ data-turbo-overlay-type-value="drawer"
14
+ data-turbo-overlay-backdrop-value="<%= backdrop %>"
15
+ <% if turbo_overlay_keep_open_on_redirect? %>data-turbo-overlay-keep-open-on-redirect="true"<% end %>
16
+ data-action="cancel->turbo-overlay#cancel click->turbo-overlay#backdropClick"
17
+ aria-labelledby="turbo-drawer-title-<%= turbo_overlay_id %>"
18
+ <% end %>>
19
+ <div class="turbo-drawer__content">
20
+ <% if !loading && content_for?(:overlay_title) %>
21
+ <header class="turbo-drawer__header">
22
+ <h2 id="turbo-drawer-title-<%= turbo_overlay_id %>" class="turbo-drawer__title">
23
+ <%= yield(:overlay_title) %>
24
+ </h2>
25
+ <% if close_button %>
26
+ <button type="button"
27
+ class="turbo-drawer__close"
28
+ data-action="turbo-overlay#close"
29
+ aria-label="Close"><span aria-hidden="true">&times;</span></button>
30
+ <% end %>
31
+ </header>
32
+ <% elsif !loading && close_button %>
33
+ <button type="button"
34
+ class="turbo-drawer__close turbo-drawer__close--floating"
35
+ data-action="turbo-overlay#close"
36
+ aria-label="Close"><span aria-hidden="true">&times;</span></button>
37
+ <% end %>
38
+
39
+ <div class="turbo-drawer__body">
40
+ <%= yield %>
41
+ </div>
42
+
43
+ <% if !loading && content_for?(:overlay_footer) %>
44
+ <footer class="turbo-drawer__footer">
45
+ <%= yield(:overlay_footer) %>
46
+ </footer>
47
+ <% end %>
48
+ </div>
49
+ </dialog>
@@ -0,0 +1,6 @@
1
+ <% loading = local_assigns.fetch(:loading, false) %>
2
+ <div popover="manual"
3
+ class="<%= class_names("turbo-overlay-hint", "turbo-hint", "turbo-overlay--loading": loading) %>"
4
+ <% if loading %>role="status" aria-live="polite" aria-label="Loading"<% else %>role="tooltip"<% end %>>
5
+ <%= yield %>
6
+ </div>
@@ -0,0 +1,12 @@
1
+ <%# Body content for an overlay's loading placeholder. Wrapped in the
2
+ matching chrome at template-emission time by `overlay_stack_tag`
3
+ (modal/drawer/popover/hint). Sizing comes from the gem's CSS so the
4
+ same partial fits all chrome contexts — hint chrome collapses the
5
+ body's min-height/padding so the spinner sits flush in the small
6
+ tooltip. Add a chrome-specific override at `_loading.html+<variant>.erb`
7
+ if you need different markup for one variant.
8
+ %>
9
+ <div class="turbo-overlay-loading__body">
10
+ <span class="turbo-overlay-loading__spinner" aria-hidden="true"></span>
11
+ <span class="turbo-overlay-loading__sr-only">Loading…</span>
12
+ </div>
@@ -0,0 +1,46 @@
1
+ <% loading = local_assigns.fetch(:loading, false) %>
2
+ <% close_button = local_assigns.fetch(:close) { overlay_close? } %>
3
+ <dialog class="<%= class_names("turbo-overlay", "turbo-overlay--modal", "turbo-modal", "turbo-overlay--loading": loading) %>"
4
+ <% if loading %>
5
+ role="status"
6
+ aria-live="polite"
7
+ aria-label="Loading"
8
+ <% else %>
9
+ data-controller="turbo-overlay"
10
+ data-turbo-overlay-id-value="<%= turbo_overlay_id %>"
11
+ data-turbo-overlay-type-value="modal"
12
+ <% if turbo_overlay_keep_open_on_redirect? %>data-turbo-overlay-keep-open-on-redirect="true"<% end %>
13
+ data-action="cancel->turbo-overlay#cancel click->turbo-overlay#backdropClick"
14
+ aria-labelledby="turbo-modal-title-<%= turbo_overlay_id %>"
15
+ <% end %>>
16
+ <div class="turbo-modal__content">
17
+ <% if !loading && content_for?(:overlay_title) %>
18
+ <header class="turbo-modal__header">
19
+ <h2 id="turbo-modal-title-<%= turbo_overlay_id %>" class="turbo-modal__title">
20
+ <%= yield(:overlay_title) %>
21
+ </h2>
22
+ <% if close_button %>
23
+ <button type="button"
24
+ class="turbo-modal__close"
25
+ data-action="turbo-overlay#close"
26
+ aria-label="Close"><span aria-hidden="true">&times;</span></button>
27
+ <% end %>
28
+ </header>
29
+ <% elsif !loading && close_button %>
30
+ <button type="button"
31
+ class="turbo-modal__close turbo-modal__close--floating"
32
+ data-action="turbo-overlay#close"
33
+ aria-label="Close"><span aria-hidden="true">&times;</span></button>
34
+ <% end %>
35
+
36
+ <div class="turbo-modal__body">
37
+ <%= yield %>
38
+ </div>
39
+
40
+ <% if !loading && content_for?(:overlay_footer) %>
41
+ <footer class="turbo-modal__footer">
42
+ <%= yield(:overlay_footer) %>
43
+ </footer>
44
+ <% end %>
45
+ </div>
46
+ </dialog>
@@ -0,0 +1,54 @@
1
+ <% loading = local_assigns.fetch(:loading, false) %>
2
+ <% position = turbo_overlay_position || TurboOverlay.configuration.popover.position %>
3
+ <% align = turbo_overlay_align || TurboOverlay.configuration.popover.align %>
4
+ <% offset = turbo_overlay_offset || TurboOverlay.configuration.popover.offset %>
5
+ <% close_button = local_assigns.fetch(:close) { defined?(@_overlay_close) && @_overlay_close != false } %>
6
+ <dialog popover="manual"
7
+ class="<%= class_names("turbo-overlay", "turbo-overlay--popover", "turbo-popover", "turbo-overlay--no-backdrop", "turbo-overlay--loading": loading) %>"
8
+ <% if loading %>
9
+ role="status"
10
+ aria-live="polite"
11
+ aria-label="Loading"
12
+ <% else %>
13
+ data-controller="turbo-overlay"
14
+ data-turbo-overlay-id-value="<%= turbo_overlay_id %>"
15
+ data-turbo-overlay-type-value="popover"
16
+ data-turbo-overlay-backdrop-value="false"
17
+ data-turbo-overlay-position-value="<%= position %>"
18
+ data-turbo-overlay-align-value="<%= align %>"
19
+ data-turbo-overlay-offset-value="<%= offset %>"
20
+ <% if turbo_overlay_keep_open_on_redirect? %>data-turbo-overlay-keep-open-on-redirect="true"<% end %>
21
+ data-action="cancel->turbo-overlay#cancel"
22
+ aria-labelledby="turbo-popover-title-<%= turbo_overlay_id %>"
23
+ <% end %>>
24
+ <div class="turbo-popover__content">
25
+ <% if !loading && content_for?(:overlay_title) %>
26
+ <header class="turbo-popover__header">
27
+ <h2 id="turbo-popover-title-<%= turbo_overlay_id %>" class="turbo-popover__title">
28
+ <%= yield(:overlay_title) %>
29
+ </h2>
30
+ <% if close_button %>
31
+ <button type="button"
32
+ class="turbo-popover__close"
33
+ data-action="turbo-overlay#close"
34
+ aria-label="Close"><span aria-hidden="true">&times;</span></button>
35
+ <% end %>
36
+ </header>
37
+ <% elsif !loading && close_button %>
38
+ <button type="button"
39
+ class="turbo-popover__close turbo-popover__close--floating"
40
+ data-action="turbo-overlay#close"
41
+ aria-label="Close"><span aria-hidden="true">&times;</span></button>
42
+ <% end %>
43
+
44
+ <div class="turbo-popover__body">
45
+ <%= yield %>
46
+ </div>
47
+
48
+ <% if !loading && content_for?(:overlay_footer) %>
49
+ <footer class="turbo-popover__footer">
50
+ <%= yield(:overlay_footer) %>
51
+ </footer>
52
+ <% end %>
53
+ </div>
54
+ </dialog>
@@ -0,0 +1,11 @@
1
+ pin "turbo_overlay", to: "turbo_overlay/index.js", preload: true
2
+ pin "turbo_overlay/setup", to: "turbo_overlay/setup.js", preload: true
3
+ pin "turbo_overlay/stack_controller", to: "turbo_overlay/stack_controller.js", preload: true
4
+ pin "turbo_overlay/overlay_controller", to: "turbo_overlay/overlay_controller.js", preload: true
5
+ pin "turbo_overlay/hint", to: "turbo_overlay/hint.js", preload: true
6
+ pin "turbo_overlay/popover_position", to: "turbo_overlay/popover_position.js", preload: true
7
+ pin "turbo_overlay/dialog_utils", to: "turbo_overlay/dialog_utils.js", preload: true
8
+ pin "turbo_overlay/history", to: "turbo_overlay/history.js", preload: true
9
+ pin "turbo_overlay/submit_close", to: "turbo_overlay/submit_close.js", preload: true
10
+ pin "turbo_overlay/options", to: "turbo_overlay/options.js", preload: true
11
+ pin "turbo_overlay/visit", to: "turbo_overlay/visit.js", preload: true
@@ -0,0 +1,115 @@
1
+ require "rails/generators/base"
2
+
3
+ module TurboOverlay
4
+ module Generators
5
+ # Copies gem-owned plumbing into the host app for full control.
6
+ # Once ejected, gem upgrades to that subset don't flow through.
7
+ # (Chrome partials are copied by the install generator and live in
8
+ # the app from day one, so they're not part of eject.)
9
+ #
10
+ # bin/rails g turbo_overlay:eject --js
11
+ # bin/rails g turbo_overlay:eject --css
12
+ # bin/rails g turbo_overlay:eject --layouts
13
+ class EjectGenerator < ::Rails::Generators::Base
14
+ GEM_ROOT = File.expand_path("../../..", __dir__)
15
+
16
+ class_option :js,
17
+ type: :boolean,
18
+ default: false,
19
+ desc: "Copy the Stimulus controllers into app/javascript/turbo_overlay/"
20
+
21
+ class_option :css,
22
+ type: :boolean,
23
+ default: false,
24
+ desc: "Copy the stylesheet into app/assets/stylesheets/turbo_overlay.css"
25
+
26
+ class_option :layouts,
27
+ type: :boolean,
28
+ default: false,
29
+ desc: "Copy the four overlay layouts into app/views/layouts/turbo_overlay/"
30
+
31
+ def print_help_if_no_flags
32
+ return if any_flag_given?
33
+
34
+ say <<~MSG
35
+
36
+ Nothing to eject. Pass one or more flags:
37
+
38
+ --js Copy Stimulus controllers to app/javascript/turbo_overlay/
39
+ --css Copy the stylesheet to app/assets/stylesheets/turbo_overlay.css
40
+ --layouts Copy modal/drawer/popover layouts to app/views/layouts/
41
+
42
+ Ejected files become app-owned — gem upgrades to those files no
43
+ longer apply. (Chrome partials are copied by `turbo_overlay:install`
44
+ and already live in your app.)
45
+
46
+ MSG
47
+ end
48
+
49
+ def eject_javascript
50
+ return unless options[:js]
51
+
52
+ copy_gem_file "app/javascript/turbo_overlay/index.js",
53
+ "app/javascript/turbo_overlay/index.js"
54
+ copy_gem_file "app/javascript/turbo_overlay/setup.js",
55
+ "app/javascript/turbo_overlay/setup.js"
56
+ copy_gem_file "app/javascript/turbo_overlay/stack_controller.js",
57
+ "app/javascript/turbo_overlay/stack_controller.js"
58
+ copy_gem_file "app/javascript/turbo_overlay/overlay_controller.js",
59
+ "app/javascript/turbo_overlay/overlay_controller.js"
60
+ copy_gem_file "app/javascript/turbo_overlay/hint.js",
61
+ "app/javascript/turbo_overlay/hint.js"
62
+ copy_gem_file "app/javascript/turbo_overlay/popover_position.js",
63
+ "app/javascript/turbo_overlay/popover_position.js"
64
+ copy_gem_file "app/javascript/turbo_overlay/submit_close.js",
65
+ "app/javascript/turbo_overlay/submit_close.js"
66
+
67
+ say <<~MSG, :yellow
68
+
69
+ JS ejected. Update your Stimulus entry to import locally:
70
+
71
+ import { register as registerTurboOverlay } from "./turbo_overlay/index"
72
+ registerTurboOverlay(application, { confirm: true })
73
+
74
+ And remove the gem's importmap pin if you had one.
75
+
76
+ MSG
77
+ end
78
+
79
+ def eject_stylesheet
80
+ return unless options[:css]
81
+
82
+ copy_gem_file "app/assets/stylesheets/turbo_overlay.css",
83
+ "app/assets/stylesheets/turbo_overlay.css"
84
+ end
85
+
86
+ def eject_layouts
87
+ return unless options[:layouts]
88
+
89
+ copy_gem_file "app/views/layouts/turbo_overlay/modal.html.erb",
90
+ "app/views/layouts/turbo_overlay/modal.html.erb"
91
+ copy_gem_file "app/views/layouts/turbo_overlay/drawer.html.erb",
92
+ "app/views/layouts/turbo_overlay/drawer.html.erb"
93
+ copy_gem_file "app/views/layouts/turbo_overlay/popover.html.erb",
94
+ "app/views/layouts/turbo_overlay/popover.html.erb"
95
+ copy_gem_file "app/views/layouts/turbo_overlay/hint.html.erb",
96
+ "app/views/layouts/turbo_overlay/hint.html.erb"
97
+ end
98
+
99
+ private
100
+
101
+ def any_flag_given?
102
+ options[:js] || options[:css] || options[:layouts]
103
+ end
104
+
105
+ def copy_gem_file(source_relative, destination_relative)
106
+ source = File.join(GEM_ROOT, source_relative)
107
+ unless File.exist?(source)
108
+ say_status :missing, source_relative, :red
109
+ return
110
+ end
111
+ create_file destination_relative, File.read(source)
112
+ end
113
+ end
114
+ end
115
+ end