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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +436 -0
- data/LICENSE.txt +21 -0
- data/README.md +330 -0
- data/Rakefile +35 -0
- data/app/assets/stylesheets/turbo_overlay.css +234 -0
- data/app/javascript/turbo_overlay/dialog_utils.js +46 -0
- data/app/javascript/turbo_overlay/hint.js +670 -0
- data/app/javascript/turbo_overlay/history.js +184 -0
- data/app/javascript/turbo_overlay/index.js +53 -0
- data/app/javascript/turbo_overlay/options.js +152 -0
- data/app/javascript/turbo_overlay/overlay_controller.js +882 -0
- data/app/javascript/turbo_overlay/popover_position.js +64 -0
- data/app/javascript/turbo_overlay/setup.js +885 -0
- data/app/javascript/turbo_overlay/stack_controller.js +131 -0
- data/app/javascript/turbo_overlay/submit_close.js +49 -0
- data/app/javascript/turbo_overlay/visit.js +52 -0
- data/app/views/layouts/turbo_overlay/drawer.html.erb +5 -0
- data/app/views/layouts/turbo_overlay/hint.html.erb +10 -0
- data/app/views/layouts/turbo_overlay/modal.html.erb +5 -0
- data/app/views/layouts/turbo_overlay/popover.html.erb +5 -0
- data/app/views/turbo_overlay/_drawer.html.erb +49 -0
- data/app/views/turbo_overlay/_hint.html.erb +6 -0
- data/app/views/turbo_overlay/_loading.html.erb +12 -0
- data/app/views/turbo_overlay/_modal.html.erb +46 -0
- data/app/views/turbo_overlay/_popover.html.erb +54 -0
- data/config/importmap.rb +11 -0
- data/lib/generators/turbo_overlay/eject_generator.rb +115 -0
- data/lib/generators/turbo_overlay/install_generator.rb +443 -0
- data/lib/generators/turbo_overlay/templates/chrome/bootstrap3/_confirm.html.erb +13 -0
- data/lib/generators/turbo_overlay/templates/chrome/bootstrap3/_drawer.html.erb +50 -0
- data/lib/generators/turbo_overlay/templates/chrome/bootstrap3/_hint.html.erb +9 -0
- data/lib/generators/turbo_overlay/templates/chrome/bootstrap3/_loading.html.erb +9 -0
- data/lib/generators/turbo_overlay/templates/chrome/bootstrap3/_modal.html.erb +49 -0
- data/lib/generators/turbo_overlay/templates/chrome/bootstrap3/_popover.html.erb +54 -0
- data/lib/generators/turbo_overlay/templates/chrome/bootstrap5/_confirm.html.erb +13 -0
- data/lib/generators/turbo_overlay/templates/chrome/bootstrap5/_drawer.html.erb +55 -0
- data/lib/generators/turbo_overlay/templates/chrome/bootstrap5/_hint.html.erb +9 -0
- data/lib/generators/turbo_overlay/templates/chrome/bootstrap5/_loading.html.erb +9 -0
- data/lib/generators/turbo_overlay/templates/chrome/bootstrap5/_modal.html.erb +58 -0
- data/lib/generators/turbo_overlay/templates/chrome/bootstrap5/_popover.html.erb +53 -0
- data/lib/generators/turbo_overlay/templates/chrome/plain/_confirm.html.erb +14 -0
- data/lib/generators/turbo_overlay/templates/chrome/tailwind/_confirm.html.erb +17 -0
- data/lib/generators/turbo_overlay/templates/chrome/tailwind/_drawer.html.erb +55 -0
- data/lib/generators/turbo_overlay/templates/chrome/tailwind/_hint.html.erb +6 -0
- data/lib/generators/turbo_overlay/templates/chrome/tailwind/_loading.html.erb +9 -0
- data/lib/generators/turbo_overlay/templates/chrome/tailwind/_modal.html.erb +46 -0
- data/lib/generators/turbo_overlay/templates/chrome/tailwind/_popover.html.erb +54 -0
- data/lib/generators/turbo_overlay/templates/initializer.rb.tt +67 -0
- data/lib/turbo_overlay/configuration.rb +226 -0
- data/lib/turbo_overlay/controller.rb +405 -0
- data/lib/turbo_overlay/engine.rb +52 -0
- data/lib/turbo_overlay/helpers/stream_helper.rb +77 -0
- data/lib/turbo_overlay/helpers/view_helper.rb +651 -0
- data/lib/turbo_overlay/version.rb +3 -0
- data/lib/turbo_overlay.rb +20 -0
- 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,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,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">×</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">×</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">×</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">×</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">×</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">×</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>
|
data/config/importmap.rb
ADDED
|
@@ -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
|