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,184 @@
|
|
|
1
|
+
// URL advance bookkeeping for turbo_overlay.
|
|
2
|
+
//
|
|
3
|
+
// `advance: true` on a modal_link_to / drawer_link_to (or a per-type
|
|
4
|
+
// config default) pushes a history entry when the overlay opens, so
|
|
5
|
+
// the URL bar reflects what the user is looking at and browser-back
|
|
6
|
+
// closes the top overlay instead of navigating away from the page
|
|
7
|
+
// beneath. Popovers and hints never advance — they're ephemeral and
|
|
8
|
+
// shouldn't churn browser history.
|
|
9
|
+
//
|
|
10
|
+
// State lives at module scope so the bookkeeping survives Stimulus
|
|
11
|
+
// disconnect/reconnect and idiomorph re-renders (the dialog node may
|
|
12
|
+
// be morphed in place; the controller instance can be re-created;
|
|
13
|
+
// the overlay id is the stable key).
|
|
14
|
+
|
|
15
|
+
// Resolved advance URL set by the click handler before the fetch
|
|
16
|
+
// goes out; consumed by the overlay controller when `shown` fires.
|
|
17
|
+
const advanceUrls = new Map()
|
|
18
|
+
|
|
19
|
+
// Overlays we've called history.pushState for. Keyed by overlay id so
|
|
20
|
+
// close-path code can ask "did we push for this overlay?" without
|
|
21
|
+
// reaching into a possibly-discarded controller instance.
|
|
22
|
+
const pushedEntries = new Map()
|
|
23
|
+
|
|
24
|
+
// Counter of popstates we caused ourselves via `history.back()` and
|
|
25
|
+
// must swallow before the popstate handler treats one as user input.
|
|
26
|
+
let expectedPopstates = 0
|
|
27
|
+
|
|
28
|
+
// Read-only accessor for setup.js's `turbo:before-visit` guard.
|
|
29
|
+
// Exposed as a function so the value reflects the current count, not
|
|
30
|
+
// a snapshot at import time.
|
|
31
|
+
export function expectedPopstateCount() {
|
|
32
|
+
return expectedPopstates
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// True if any currently-tracked overlay entry on the stack has a
|
|
36
|
+
// `pushed` record. Used by setup.js to decide whether to cancel a
|
|
37
|
+
// Turbo restoration visit that was triggered by popstate over an
|
|
38
|
+
// advance-pushed entry.
|
|
39
|
+
export function hasPushedOverlayOnStack() {
|
|
40
|
+
if (!stackController || !stackController.entries) return false
|
|
41
|
+
for (const entry of stackController.entries) {
|
|
42
|
+
if (entry && pushedEntries.has(entry.id)) return true
|
|
43
|
+
}
|
|
44
|
+
return false
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Live reference to the per-page stack controller. The stack
|
|
48
|
+
// Stimulus controller calls `setStackController(this)` on connect and
|
|
49
|
+
// `setStackController(null)` on disconnect. Used by the popstate
|
|
50
|
+
// handler to walk the stack without reaching into Stimulus internals
|
|
51
|
+
// from setup.js (which is not itself a controller).
|
|
52
|
+
let stackController = null
|
|
53
|
+
|
|
54
|
+
export function setStackController(controller) {
|
|
55
|
+
stackController = controller
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getStackController() {
|
|
59
|
+
return stackController
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function setAdvanceUrl(id, url) {
|
|
63
|
+
if (!id) return
|
|
64
|
+
if (url) advanceUrls.set(id, url)
|
|
65
|
+
else advanceUrls.delete(id)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function getAdvanceUrl(id) {
|
|
69
|
+
return id ? (advanceUrls.get(id) || null) : null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function clearAdvanceUrl(id) {
|
|
73
|
+
if (id) advanceUrls.delete(id)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function markPushed(id, url, type) {
|
|
77
|
+
if (!id) return
|
|
78
|
+
pushedEntries.set(id, { url, type })
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function isPushed(id) {
|
|
82
|
+
return !!(id && pushedEntries.has(id))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function clearPushed(id) {
|
|
86
|
+
if (id) pushedEntries.delete(id)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Count entries from a stack snapshot whose id has a `pushed` record.
|
|
90
|
+
// Used by close-path code to detect whether a given close actually
|
|
91
|
+
// reduced the live pushed count (and therefore should reverse history).
|
|
92
|
+
export function livePushedCount(stackEntries) {
|
|
93
|
+
if (!stackEntries || !stackEntries.length) return 0
|
|
94
|
+
let n = 0
|
|
95
|
+
for (const e of stackEntries) {
|
|
96
|
+
if (e && pushedEntries.has(e.id)) n += 1
|
|
97
|
+
}
|
|
98
|
+
return n
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function pushOverlayState(id, type, url) {
|
|
102
|
+
if (typeof window === "undefined" || !window.history) return
|
|
103
|
+
try {
|
|
104
|
+
window.history.pushState(
|
|
105
|
+
{ turboOverlay: { id, type } },
|
|
106
|
+
"",
|
|
107
|
+
url
|
|
108
|
+
)
|
|
109
|
+
} catch (_) {
|
|
110
|
+
// pushState can throw on cross-origin URLs; treat as "didn't push"
|
|
111
|
+
// so the close path won't try to reverse a history entry that
|
|
112
|
+
// doesn't exist. The caller is expected to skip markPushed too if
|
|
113
|
+
// this throws — but we're defensive: the caller's surrounding
|
|
114
|
+
// try/catch will handle it.
|
|
115
|
+
throw _
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function reverseHistoryForClose() {
|
|
120
|
+
if (typeof window === "undefined" || !window.history) return
|
|
121
|
+
expectedPopstates += 1
|
|
122
|
+
try {
|
|
123
|
+
window.history.back()
|
|
124
|
+
} catch (_) {
|
|
125
|
+
// Same defensive note as pushOverlayState. If back() throws we'll
|
|
126
|
+
// have an over-counted expectedPopstates that one stray popstate
|
|
127
|
+
// (if it ever fires) would swallow. Acceptable.
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Wipe state at navigation boundaries. The pushed history entries the
|
|
132
|
+
// gem created remain in the session-history backing store — the user
|
|
133
|
+
// could still navigate back to them — but the gem no longer tracks
|
|
134
|
+
// them, so a popstate landing on one of those URLs after navigation
|
|
135
|
+
// is treated as a regular user-initiated history navigation (no
|
|
136
|
+
// overlay matches; we no-op).
|
|
137
|
+
export function resetHistoryState() {
|
|
138
|
+
advanceUrls.clear()
|
|
139
|
+
pushedEntries.clear()
|
|
140
|
+
expectedPopstates = 0
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function registerPopstateHandler() {
|
|
144
|
+
if (typeof window === "undefined") return
|
|
145
|
+
if (window._turboOverlayPopstateRegistered) return
|
|
146
|
+
window._turboOverlayPopstateRegistered = true
|
|
147
|
+
|
|
148
|
+
// Two responsibilities:
|
|
149
|
+
//
|
|
150
|
+
// 1. If we caused this popstate via `history.back()` (close path),
|
|
151
|
+
// just decrement the counter. The matching overlay was already
|
|
152
|
+
// cleared from `pushedEntries` before history.back().
|
|
153
|
+
//
|
|
154
|
+
// 2. If the user pressed browser-back over an advance overlay's
|
|
155
|
+
// pushed URL, close the topmost overlay whose id is still in
|
|
156
|
+
// `pushedEntries`. The overlay's close path sees `_closedByBack`
|
|
157
|
+
// and skips its own `history.back()` so we don't double-pop.
|
|
158
|
+
//
|
|
159
|
+
// Note: we can't stop Turbo Drive's own popstate handler from
|
|
160
|
+
// running (for window-only events, capture/bubble flags don't
|
|
161
|
+
// change at-target order; Turbo's handler is registered first via
|
|
162
|
+
// its own import order and runs first). The protection against
|
|
163
|
+
// Turbo's `historyPoppedWithEmptyState` → `before-cache` → teardown
|
|
164
|
+
// chain lives in setup.js, which gates `tearDownAllOverlays` on
|
|
165
|
+
// whether a real Turbo visit is actually in progress.
|
|
166
|
+
window.addEventListener("popstate", () => {
|
|
167
|
+
if (expectedPopstates > 0) {
|
|
168
|
+
expectedPopstates -= 1
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
const stack = stackController
|
|
172
|
+
if (!stack || !stack.entries) return
|
|
173
|
+
for (let i = stack.entries.length - 1; i >= 0; i--) {
|
|
174
|
+
const entry = stack.entries[i]
|
|
175
|
+
if (!entry || !entry.controller) continue
|
|
176
|
+
if (!pushedEntries.has(entry.id)) continue
|
|
177
|
+
entry.controller._closedByBack = true
|
|
178
|
+
if (typeof entry.controller.close === "function") {
|
|
179
|
+
entry.controller.close()
|
|
180
|
+
}
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import "turbo_overlay/setup"
|
|
2
|
+
import "turbo_overlay/hint"
|
|
3
|
+
import { registerConfirm } from "turbo_overlay/setup"
|
|
4
|
+
import StackController from "turbo_overlay/stack_controller"
|
|
5
|
+
import OverlayController from "turbo_overlay/overlay_controller"
|
|
6
|
+
import { visit } from "turbo_overlay/visit"
|
|
7
|
+
|
|
8
|
+
// Single entry point for the gem's Stimulus controllers.
|
|
9
|
+
//
|
|
10
|
+
// import { register } from "turbo_overlay"
|
|
11
|
+
// register(application)
|
|
12
|
+
//
|
|
13
|
+
// Registers two controllers under their canonical identifiers:
|
|
14
|
+
// `turbo-overlay-stack` and `turbo-overlay`.
|
|
15
|
+
//
|
|
16
|
+
// Importing this module also runs `turbo_overlay/setup` (page-level
|
|
17
|
+
// wiring: request-header injection, loading placeholders, back/forward
|
|
18
|
+
// cache teardown, the `turbo_stream.overlay` custom stream action) and
|
|
19
|
+
// `turbo_overlay/hint` (hover-triggered preview module). Both
|
|
20
|
+
// self-bootstrap and are guarded by `window._turboOverlay*Registered`
|
|
21
|
+
// flags, so importing this entry point more than once is safe.
|
|
22
|
+
//
|
|
23
|
+
// Pass `{ confirm: true }` to also route `data-turbo-confirm` on
|
|
24
|
+
// links/forms through the gem's themed overlay instead of the
|
|
25
|
+
// browser-native `window.confirm`. Requires confirm chrome partials
|
|
26
|
+
// in `app/views/turbo_overlay/` (copied by `turbo_overlay:install`);
|
|
27
|
+
// falls back to native `confirm` if no variant partial is present.
|
|
28
|
+
//
|
|
29
|
+
// register(application, { confirm: true })
|
|
30
|
+
//
|
|
31
|
+
// The hint module is loaded unconditionally — it's inert until a link
|
|
32
|
+
// carrying `data-turbo-overlay-hint` is hovered.
|
|
33
|
+
export function register(application, options = {}) {
|
|
34
|
+
application.register("turbo-overlay-stack", StackController)
|
|
35
|
+
application.register("turbo-overlay", OverlayController)
|
|
36
|
+
if (options.confirm) registerConfirm()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export { StackController, OverlayController, registerConfirm, visit }
|
|
40
|
+
|
|
41
|
+
// Expose a window global so non-bundler callers — inline `onclick`,
|
|
42
|
+
// third-party callbacks (Google Maps markers, Leaflet popups), custom
|
|
43
|
+
// elements that don't import from this package — can call
|
|
44
|
+
// `TurboOverlay.visit(url, opts)` without a module import. Assigned at
|
|
45
|
+
// import time, not inside `register()`, so it's available the moment
|
|
46
|
+
// the gem's JS loads.
|
|
47
|
+
if (typeof window !== "undefined") {
|
|
48
|
+
window.TurboOverlay = Object.assign(window.TurboOverlay || {}, {
|
|
49
|
+
visit,
|
|
50
|
+
register,
|
|
51
|
+
registerConfirm,
|
|
52
|
+
})
|
|
53
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// Single source of truth for the overlay trigger contract — option
|
|
2
|
+
// names, dataset attribute names, request header names, encoding rules,
|
|
3
|
+
// type restrictions, plus the validation + dataset-build helpers that
|
|
4
|
+
// the JS-facing `visit()` API relies on.
|
|
5
|
+
//
|
|
6
|
+
// Three places write or read these mappings:
|
|
7
|
+
// 1. Ruby view helpers (`view_helper.rb`) — write dataset attributes
|
|
8
|
+
// when rendering `modal_link_to` / `drawer_link_to` /
|
|
9
|
+
// `popover_link_to`. Authoritative on the server side; Ruby can't
|
|
10
|
+
// share this table.
|
|
11
|
+
// 2. `visit.js` — writes dataset attributes on a synthesized link
|
|
12
|
+
// when JS code calls `TurboOverlay.visit(url, opts)`. Uses
|
|
13
|
+
// `buildOverlayDataset` and `validateVisitArgs` from this file.
|
|
14
|
+
// 3. `setup.js` (turbo:before-fetch-request hook) — reads dataset
|
|
15
|
+
// attributes off the trigger and emits the X-Turbo-Overlay-*
|
|
16
|
+
// headers via `emitOverlayHeaders` from this file.
|
|
17
|
+
//
|
|
18
|
+
// (2) and (3) both consume this table. Adding a header-bearing option
|
|
19
|
+
// here is enough to make it round-trip through `visit()` and the fetch
|
|
20
|
+
// hook. The Ruby helper must still be updated in parallel.
|
|
21
|
+
//
|
|
22
|
+
// This module deliberately has no imports so it stays unit-testable
|
|
23
|
+
// under plain `node --test` (no DOM, no importmap).
|
|
24
|
+
//
|
|
25
|
+
// Encoding rules mirror `_overlay_normalize_link_args` at
|
|
26
|
+
// `lib/turbo_overlay/helpers/view_helper.rb:454-482`:
|
|
27
|
+
// - `backdrop` / `close` only emit on `false` (the non-default)
|
|
28
|
+
// - `keepOpenOnRedirect` only emits on `true` (the non-default)
|
|
29
|
+
// - `advance` is restricted to modal/drawer
|
|
30
|
+
// - everything else emits whenever a value is supplied
|
|
31
|
+
|
|
32
|
+
export const OVERLAY_OPTIONS = [
|
|
33
|
+
{
|
|
34
|
+
key: "id",
|
|
35
|
+
dataset: "turboOverlayId",
|
|
36
|
+
header: "X-Turbo-Overlay-Id",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
key: "position",
|
|
40
|
+
dataset: "turboOverlayPosition",
|
|
41
|
+
header: "X-Turbo-Overlay-Position",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
key: "align",
|
|
45
|
+
dataset: "turboOverlayAlign",
|
|
46
|
+
header: "X-Turbo-Overlay-Align",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
key: "offset",
|
|
50
|
+
dataset: "turboOverlayOffset",
|
|
51
|
+
header: "X-Turbo-Overlay-Offset",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
key: "backdrop",
|
|
55
|
+
dataset: "turboOverlayBackdrop",
|
|
56
|
+
header: "X-Turbo-Overlay-Backdrop",
|
|
57
|
+
onlyWhen: (v) => v === false,
|
|
58
|
+
encode: () => "false",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
key: "close",
|
|
62
|
+
dataset: "turboOverlayClose",
|
|
63
|
+
header: "X-Turbo-Overlay-Close",
|
|
64
|
+
onlyWhen: (v) => v === false,
|
|
65
|
+
encode: () => "false",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
key: "keepOpenOnRedirect",
|
|
69
|
+
dataset: "turboOverlayKeepOpenOnRedirect",
|
|
70
|
+
header: "X-Turbo-Overlay-Keep-Open",
|
|
71
|
+
onlyWhen: (v) => v === true,
|
|
72
|
+
encode: () => "true",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
key: "advance",
|
|
76
|
+
dataset: "turboOverlayAdvance",
|
|
77
|
+
onlyTypes: ["modal", "drawer"],
|
|
78
|
+
encode: (v) => (v === true ? "true" : v === false ? "false" : String(v)),
|
|
79
|
+
},
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
export const VALID_TYPES = ["modal", "drawer", "popover", "hint"]
|
|
83
|
+
|
|
84
|
+
// Writes dataset attributes on `el` for each option in `opts` whose
|
|
85
|
+
// entry passes the table's gating rules. `type` is the overlay type
|
|
86
|
+
// (modal/drawer/popover/hint) and is used to honor `onlyTypes`.
|
|
87
|
+
//
|
|
88
|
+
// Returns nothing; mutates `el.dataset` in place. `el` is duck-typed —
|
|
89
|
+
// any object with a `.dataset` property works.
|
|
90
|
+
export function applyOverlayOptions(el, type, opts) {
|
|
91
|
+
for (const entry of OVERLAY_OPTIONS) {
|
|
92
|
+
const value = opts[entry.key]
|
|
93
|
+
if (value === undefined || value === null) continue
|
|
94
|
+
if (entry.onlyTypes && !entry.onlyTypes.includes(type)) continue
|
|
95
|
+
if (entry.onlyWhen && !entry.onlyWhen(value)) continue
|
|
96
|
+
el.dataset[entry.dataset] = entry.encode ? entry.encode(value) : String(value)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Reads dataset attributes from a trigger element and writes the
|
|
101
|
+
// corresponding X-Turbo-Overlay-* headers into `headers`. Only entries
|
|
102
|
+
// that declare a `header` participate (e.g. `advance` is dataset-only —
|
|
103
|
+
// the server reads it from the response context, not a request header).
|
|
104
|
+
export function emitOverlayHeaders(dataset, headers) {
|
|
105
|
+
for (const entry of OVERLAY_OPTIONS) {
|
|
106
|
+
if (!entry.header) continue
|
|
107
|
+
const value = dataset[entry.dataset]
|
|
108
|
+
if (value === undefined || value === null || value === "") continue
|
|
109
|
+
headers[entry.header] = value
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Returns the dataset object that a trigger element synthesized by
|
|
114
|
+
// `visit()` should carry. Includes the three always-present attributes
|
|
115
|
+
// (`turboStream`, `turboOverlay`, `turboFrame`) plus everything written
|
|
116
|
+
// by `applyOverlayOptions`.
|
|
117
|
+
export function buildOverlayDataset(type, options = {}) {
|
|
118
|
+
const target = { dataset: {} }
|
|
119
|
+
target.dataset.turboStream = "true"
|
|
120
|
+
target.dataset.turboOverlay = type
|
|
121
|
+
target.dataset.turboFrame = options.frame || "_top"
|
|
122
|
+
applyOverlayOptions(target, type, options)
|
|
123
|
+
return target.dataset
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Validates the (url, options) pair `visit()` receives. Throws
|
|
127
|
+
// TypeError on misuse. Returns the resolved overlay type so callers
|
|
128
|
+
// don't have to re-derive it.
|
|
129
|
+
//
|
|
130
|
+
// Popovers must carry an `anchor` element — the synthesized trigger
|
|
131
|
+
// link has no useful bounding rect, so `setup.js` reads the anchor off
|
|
132
|
+
// `link.__turboOverlayAnchor` to position the popover. We skip the
|
|
133
|
+
// `instanceof Element` check when `Element` is undefined (the node test
|
|
134
|
+
// environment) so the pure validation can be unit-tested with a duck.
|
|
135
|
+
export function validateVisitArgs(url, options) {
|
|
136
|
+
if (typeof url !== "string" || url.length === 0) {
|
|
137
|
+
throw new TypeError("TurboOverlay.visit: url must be a non-empty string")
|
|
138
|
+
}
|
|
139
|
+
const type = options.type || "modal"
|
|
140
|
+
if (!VALID_TYPES.includes(type)) {
|
|
141
|
+
throw new TypeError(
|
|
142
|
+
`TurboOverlay.visit: invalid type "${type}" (expected one of ${VALID_TYPES.join(", ")})`
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
if (type === "popover") {
|
|
146
|
+
const ElementCtor = typeof Element === "function" ? Element : null
|
|
147
|
+
if (!options.anchor || (ElementCtor && !(options.anchor instanceof ElementCtor))) {
|
|
148
|
+
throw new TypeError("TurboOverlay.visit: popover requires an `anchor` element")
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return type
|
|
152
|
+
}
|