studio-engine 0.100.0.rc1 → 0.101.0.rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +42 -0
- data/app/javascript/studio/controllers/hold_button_controller.js +1 -1
- data/app/javascript/studio/hold_button_hooks.js +51 -110
- data/app/views/layouts/_navbar.html.erb +5 -1
- data/app/views/studio/_hold_button.html.erb +10 -32
- data/lib/studio/hold_button.rb +32 -0
- data/lib/studio/version.rb +1 -1
- data/lib/studio.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2c4e934f3894dd00a374f95b4fbe030e95fcb1f2fc88a5838703c5aed25244c5
|
|
4
|
+
data.tar.gz: ece60cbff86a6e9bfe6436fbf41fedd8ef0f89a3c1bb907fc5f6990e07910cf2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 596c703216999cf57ceaa621439093da0c4763b38c8fc4e60147594d623920964bf4d2315a947eb82aeeedba70713a01d00beef3e76351cc640af296f434756d
|
|
7
|
+
data.tar.gz: 9ec306df7f9f0375c5056aaa895594874cd94b10c486cefc84fa2ca69d383deb993b54c892383ccb020da65efb7851cdef58885e9677abdd33eedb5ebe2619fd
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,48 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.101.0 — 2026-10-10
|
|
8
|
+
|
|
9
|
+
### Breaking
|
|
10
|
+
|
|
11
|
+
- **The hold button takes no JavaScript-string locals.** `studio/_hold_button`
|
|
12
|
+
no longer writes them to `data-*` attributes and `studio/hold_button_hooks`
|
|
13
|
+
no longer evaluates them, so the button needs no `unsafe-eval`. Each removed
|
|
14
|
+
local has an event the button dispatches (they bubble, and every
|
|
15
|
+
`event.detail` carries `{ id }`, the button's `hold_id`):
|
|
16
|
+
|
|
17
|
+
| Removed local | Attribute it wrote | Answer this event |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| `guard:` | `data-guard` | `hold-button:guard`: `preventDefault()` refuses the hold |
|
|
20
|
+
| `on_hold_start:` | `data-on-hold-start` | `hold-button:start` |
|
|
21
|
+
| `validate:` | `data-validate` | `hold-button:validate`: `event.detail.waitUntil(answer)`, a boolean or a promise of one |
|
|
22
|
+
| `early_action:` | `data-early-action` | `hold-button:early`: `preventDefault()` takes the action over |
|
|
23
|
+
| `early_action_guard:` | `data-early-action-guard` | `hold-button:early`: decide in the listener, and call `preventDefault()` only when the action runs |
|
|
24
|
+
| `on_success:` | `data-on-success` | `hold-button:success`: `preventDefault()` keeps the button's state for the listener |
|
|
25
|
+
|
|
26
|
+
`validate_at:` and `early_action_at:` stay, and time the events.
|
|
27
|
+
|
|
28
|
+
A call site that still passes one fails loudly:
|
|
29
|
+
- `render "studio/hold_button"` with a removed local that is not `nil` raises
|
|
30
|
+
`ArgumentError` naming each local and its event, in every environment
|
|
31
|
+
(`Studio::HoldButton.refuse_removed_locals!`).
|
|
32
|
+
- A `.hold-btn` that carries one of the attributes by hand refuses every
|
|
33
|
+
press, dispatches nothing, and logs a console error naming the attribute
|
|
34
|
+
and its event.
|
|
35
|
+
|
|
36
|
+
`SUCCESS_SETTLE_MS` is no longer exported by `studio/hold_button_hooks`:
|
|
37
|
+
`hold-button:success` is dispatched at the full duration, with no half-second
|
|
38
|
+
delay. The module no longer reads `button.dataset`, so a consumer test that
|
|
39
|
+
scans it for the attribute names it evaluates finds none.
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
|
|
43
|
+
- **The navbar takes a `brand_heading` local.** `layouts/_navbar` draws the
|
|
44
|
+
brand title as the page's `<h1>` by default, exactly as before. Pass
|
|
45
|
+
`brand_heading: false` on a page that carries its own h1 and the brand is drawn
|
|
46
|
+
as a `<div>` with the same classes and contents; no other byte of the navbar
|
|
47
|
+
changes. Styling keys on `.nav-title`, never on the element name.
|
|
48
|
+
|
|
7
49
|
## 0.100.0 — 2026-10-10
|
|
8
50
|
|
|
9
51
|
### Added
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// touchend->hold-button#end touchcancel->hold-button#end">
|
|
11
11
|
//
|
|
12
12
|
// The timeline, the idle nudge and the fizz portal are studio/hold_button; the
|
|
13
|
-
// events
|
|
13
|
+
// events the timeline asks through are studio/hold_button_hooks.
|
|
14
14
|
import { Controller } from "@hotwired/stimulus"
|
|
15
15
|
import { Hold, mountPortal } from "studio/hold_button"
|
|
16
16
|
import { holdHooks } from "studio/hold_button_hooks"
|
|
@@ -1,70 +1,50 @@
|
|
|
1
1
|
// studio/hold_button_hooks: how one hold button asks its page the timeline's
|
|
2
|
-
// questions (studio/hold_button).
|
|
2
|
+
// questions (studio/hold_button). It dispatches events on the button, which
|
|
3
|
+
// bubble:
|
|
3
4
|
//
|
|
4
|
-
//
|
|
5
|
+
// hold-button:guard at the press. preventDefault() refuses the hold.
|
|
6
|
+
// hold-button:start the hold began.
|
|
7
|
+
// hold-button:validate at validate_at. event.detail.waitUntil(answer)
|
|
8
|
+
// takes a boolean or a promise of one; a false or
|
|
9
|
+
// failed answer aborts the hold.
|
|
10
|
+
// hold-button:early at early_action_at. preventDefault() takes the
|
|
11
|
+
// action over: the hold never completes.
|
|
12
|
+
// hold-button:success at the full duration. preventDefault() keeps the
|
|
13
|
+
// button in its holding state for the listener to
|
|
14
|
+
// resolve; otherwise it shows its success face.
|
|
5
15
|
//
|
|
6
|
-
//
|
|
7
|
-
// hold-button:start the hold began.
|
|
8
|
-
// hold-button:validate at validate_at. event.detail.waitUntil(answer)
|
|
9
|
-
// takes a boolean or a promise of one; a false or
|
|
10
|
-
// failed answer aborts the hold.
|
|
11
|
-
// hold-button:early at early_action_at. preventDefault() takes the
|
|
12
|
-
// action over: the hold never completes.
|
|
13
|
-
// hold-button:success at the full duration. preventDefault() keeps the
|
|
14
|
-
// button in its holding state for the listener to
|
|
15
|
-
// resolve; otherwise it shows its success face.
|
|
16
|
+
// Every event's detail carries { id }, the button's hold_id.
|
|
16
17
|
//
|
|
17
|
-
//
|
|
18
|
+
// <div @hold-button:guard="ready || $event.preventDefault()"
|
|
19
|
+
// @hold-button:success="submit()">
|
|
20
|
+
// <%= render "studio/hold_button", hold_id: "confirm" %>
|
|
21
|
+
// </div>
|
|
18
22
|
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// </div>
|
|
23
|
+
// It evaluates nothing. A button that carries one of REMOVED_HOOKS' attributes
|
|
24
|
+
// was written for the JavaScript-string locals this module no longer runs: every
|
|
25
|
+
// press on it is refused and says so on the console.
|
|
23
26
|
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
// data-* attributes and are evaluated against the nearest [x-data] scope
|
|
27
|
-
// with `d` and `data` aliased to its data. They stay while a consumer
|
|
28
|
-
// passes them (turf-monster does), and go when none does.
|
|
29
|
-
//
|
|
30
|
-
// Either voice can refuse: a hold needs every guard to pass. With an [x-data]
|
|
31
|
-
// ancestor a press is also refused while that scope's `submitting` is true, so
|
|
32
|
-
// a second hold cannot double-submit.
|
|
27
|
+
// With an [x-data] ancestor a press is also refused while that scope's
|
|
28
|
+
// `submitting` is true, so a second hold cannot double-submit.
|
|
33
29
|
//
|
|
34
30
|
// It imports nothing; test/javascript/hold_button_hooks.test.mjs loads it as a
|
|
35
31
|
// data: module.
|
|
36
32
|
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
export const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return Alpine.evaluate(scope, expression, { scope: { d: data, data } })
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// The same for an expression that may return a promise. Alpine.evaluate
|
|
50
|
-
// returns before an async expression settles, so this compiles its own
|
|
51
|
-
// function with the names an expression may use.
|
|
52
|
-
function evaluateAsync(Alpine, win, scope, expression) {
|
|
53
|
-
const data = Alpine.$data(scope)
|
|
54
|
-
try {
|
|
55
|
-
const fn = new AsyncFunction("d", "data", "Alpine", "window", '"use strict"; return (' + expression + ");")
|
|
56
|
-
return fn(data, data, Alpine, win)
|
|
57
|
-
} catch (error) {
|
|
58
|
-
return Promise.reject(error)
|
|
59
|
-
}
|
|
33
|
+
// Attribute => [the removed local that wrote it, the event that answers now].
|
|
34
|
+
// Studio::HoldButton::REMOVED_LOCALS is the same table for the partial.
|
|
35
|
+
export const REMOVED_HOOKS = {
|
|
36
|
+
"data-guard": ["guard", "hold-button:guard"],
|
|
37
|
+
"data-on-hold-start": ["on_hold_start", "hold-button:start"],
|
|
38
|
+
"data-validate": ["validate", "hold-button:validate"],
|
|
39
|
+
"data-early-action": ["early_action", "hold-button:early"],
|
|
40
|
+
"data-early-action-guard": ["early_action_guard", "hold-button:early"],
|
|
41
|
+
"data-on-success": ["on_success", "hold-button:success"]
|
|
60
42
|
}
|
|
61
43
|
|
|
62
44
|
// The hooks a Hold takes, for one button.
|
|
63
45
|
export function holdHooks(button, { win } = {}) {
|
|
64
46
|
const host = win || globalThis
|
|
65
|
-
const id = () => button.
|
|
66
|
-
const warn = (what, error) => host.console.warn(`[hold:${id()}] ${what}`, error || "")
|
|
67
|
-
const fail = (what, error) => host.console.error(`[hold:${id()}] ${what}`, error)
|
|
47
|
+
const id = () => button.getAttribute("data-hold-id") || "hold"
|
|
68
48
|
|
|
69
49
|
const fire = (name, detail = {}) => {
|
|
70
50
|
const event = new host.CustomEvent(`hold-button:${name}`, {
|
|
@@ -74,89 +54,50 @@ export function holdHooks(button, { win } = {}) {
|
|
|
74
54
|
return event
|
|
75
55
|
}
|
|
76
56
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
57
|
+
// True, and reported, when the button carries a removed hook.
|
|
58
|
+
const stale = () => {
|
|
59
|
+
const found = Object.keys(REMOVED_HOOKS).filter((attribute) => button.hasAttribute(attribute))
|
|
60
|
+
for (const attribute of found) {
|
|
61
|
+
const [local, event] = REMOVED_HOOKS[attribute]
|
|
62
|
+
host.console.error(`[hold:${id()}] ${attribute} (the ${local}: local) is no longer evaluated; ` +
|
|
63
|
+
`answer ${event} instead. The hold is refused.`)
|
|
64
|
+
}
|
|
65
|
+
return found.length > 0
|
|
83
66
|
}
|
|
84
67
|
|
|
85
68
|
return {
|
|
86
|
-
// Refused by the scope's `submitting`,
|
|
87
|
-
// hold-button:guard listener.
|
|
88
|
-
// refuses: the caller asked for a check that did not run.
|
|
69
|
+
// Refused by a removed hook, by the scope's `submitting`, or by a
|
|
70
|
+
// hold-button:guard listener.
|
|
89
71
|
guard() {
|
|
90
|
-
|
|
72
|
+
if (stale()) return false
|
|
73
|
+
const found = host.Alpine ? button.closest("[x-data]") : null
|
|
91
74
|
if (found) {
|
|
92
75
|
const data = host.Alpine.$data(found)
|
|
93
76
|
if (data && data.submitting) return false
|
|
94
77
|
}
|
|
95
|
-
const expression = button.dataset.guard
|
|
96
|
-
if (expression) {
|
|
97
|
-
if (!found) { warn("guard: no Alpine scope to evaluate it in; the hold is refused"); return false }
|
|
98
|
-
if (!evaluate(host.Alpine, found, expression)) return false
|
|
99
|
-
}
|
|
100
78
|
return !fire("guard").defaultPrevented
|
|
101
79
|
},
|
|
102
80
|
|
|
103
81
|
started() {
|
|
104
|
-
const expression = button.dataset.onHoldStart
|
|
105
|
-
const found = expression ? scopeFor("on_hold_start") : null
|
|
106
|
-
if (found) {
|
|
107
|
-
try { evaluate(host.Alpine, found, expression) } catch (error) { fail("on_hold_start threw", error) }
|
|
108
|
-
}
|
|
109
82
|
fire("start")
|
|
110
83
|
},
|
|
111
84
|
|
|
112
|
-
// True unless
|
|
113
|
-
//
|
|
85
|
+
// True unless a hold-button:validate listener says no. Rejects when one
|
|
86
|
+
// fails, which aborts the hold.
|
|
114
87
|
validate() {
|
|
115
88
|
const answers = []
|
|
116
|
-
const expression = button.dataset.validate
|
|
117
|
-
if (expression) {
|
|
118
|
-
const found = scopeFor("validate")
|
|
119
|
-
if (found) answers.push(evaluateAsync(host.Alpine, host, found, expression))
|
|
120
|
-
}
|
|
121
89
|
fire("validate", { waitUntil: (answer) => { answers.push(answer) } })
|
|
122
90
|
return Promise.all(answers).then((all) => all.every(Boolean))
|
|
123
91
|
},
|
|
124
92
|
|
|
125
|
-
// True when
|
|
126
|
-
// the action over.
|
|
93
|
+
// True when a hold-button:early listener took the action over.
|
|
127
94
|
early() {
|
|
128
|
-
|
|
129
|
-
const expression = button.dataset.earlyAction
|
|
130
|
-
const found = expression ? scopeFor("early_action") : null
|
|
131
|
-
if (found) {
|
|
132
|
-
const guard = button.dataset.earlyActionGuard
|
|
133
|
-
if (!guard || evaluate(host.Alpine, found, guard)) {
|
|
134
|
-
taken = true
|
|
135
|
-
try { evaluate(host.Alpine, found, expression) } catch (error) { fail("early_action threw", error) }
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
return fire("early").defaultPrevented || taken
|
|
95
|
+
return fire("early").defaultPrevented
|
|
139
96
|
},
|
|
140
97
|
|
|
141
|
-
// True when a
|
|
142
|
-
// on_success runs SUCCESS_SETTLE_MS later, with the button still in its
|
|
143
|
-
// holding state; if it throws, the button shows its success face.
|
|
98
|
+
// True when a listener owns the button's state from here.
|
|
144
99
|
completed() {
|
|
145
|
-
|
|
146
|
-
const expression = button.dataset.onSuccess
|
|
147
|
-
if (!expression) return event.defaultPrevented
|
|
148
|
-
|
|
149
|
-
host.setTimeout(() => {
|
|
150
|
-
try {
|
|
151
|
-
const found = scopeFor("on_success")
|
|
152
|
-
if (found) evaluate(host.Alpine, found, expression)
|
|
153
|
-
} catch (error) {
|
|
154
|
-
fail("on_success threw", error)
|
|
155
|
-
button.classList.remove("process")
|
|
156
|
-
button.classList.add("success")
|
|
157
|
-
}
|
|
158
|
-
}, SUCCESS_SETTLE_MS)
|
|
159
|
-
return true
|
|
100
|
+
return fire("success").defaultPrevented
|
|
160
101
|
}
|
|
161
102
|
}
|
|
162
103
|
}
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
balance_html — raw HTML for balance display (passed through to _user_nav)
|
|
6
6
|
extra_icons_html — raw HTML for extra icon buttons (passed through to _user_nav)
|
|
7
7
|
show_logout_link — boolean, show "Log out" link in user nav (passed through to _user_nav)
|
|
8
|
+
brand_heading — boolean, draw the brand title as the page's h1 (default: true).
|
|
9
|
+
Pass false on a page that carries its own h1: the brand is
|
|
10
|
+
drawn as a div with the same classes and contents.
|
|
8
11
|
%>
|
|
9
12
|
<% show_user = local_assigns.fetch(:show_logged_in, logged_in?) %>
|
|
10
13
|
<% is_preview = local_assigns.fetch(:preview, false) %>
|
|
@@ -19,6 +22,7 @@
|
|
|
19
22
|
<% balance_html = local_assigns.fetch(:balance_html, nil) %>
|
|
20
23
|
<% extra_icons_html = local_assigns.fetch(:extra_icons_html, nil) %>
|
|
21
24
|
<% show_logout_link = local_assigns.fetch(:show_logout_link, false) %>
|
|
25
|
+
<% brand_tag = local_assigns.fetch(:brand_heading, true) ? "h1" : "div" %>
|
|
22
26
|
<%# Out-of-the-box navigation (engine 0.30): when the host declares
|
|
23
27
|
Studio.sidebar_sections, the navbar mounts the link-sidebar trigger in its
|
|
24
28
|
icon rails and renders the slide-out panels after the header. With no
|
|
@@ -150,7 +154,7 @@
|
|
|
150
154
|
overflow that has to be clipped belongs to each word's own box —
|
|
151
155
|
`truncate` on the h1 would clip the flex CONTAINER and leave the
|
|
152
156
|
spans drawing outside it, which is the bug rather than the fix. %>
|
|
153
|
-
|
|
157
|
+
<<%= brand_tag %> class="nav-title font-extrabold text-heading tracking-tight min-w-0"><span class="truncate"><%= prefix %></span><span class="text-primary truncate"><%= last_word %></span></<%= brand_tag %>>
|
|
154
158
|
<% end %>
|
|
155
159
|
<%# Desktop nav — Studio.navbar_links %>
|
|
156
160
|
<nav class="hidden md:flex items-center gap-4"<%= ' aria-label="Main"'.html_safe if navbar_links.any? %>>
|
|
@@ -20,11 +20,9 @@
|
|
|
20
20
|
hold-button:success at the full duration; preventDefault() leaves the
|
|
21
21
|
button's state to the listener
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
early_action_guard
|
|
25
|
-
|
|
26
|
-
and `data` aliased to its data. They stay while a consumer passes them;
|
|
27
|
-
write new call sites against the events.
|
|
23
|
+
It takes no JavaScript: the guard, on_hold_start, validate, early_action,
|
|
24
|
+
early_action_guard and on_success locals are removed, and passing one raises
|
|
25
|
+
(Studio::HoldButton). Answer the event instead.
|
|
28
26
|
|
|
29
27
|
Locals:
|
|
30
28
|
default_text - Button label in idle state (default: "Hold to Confirm")
|
|
@@ -32,21 +30,10 @@
|
|
|
32
30
|
success_text - Text shown on completion (default: "Confirmed!")
|
|
33
31
|
duration - Hold duration in ms (default: 2000)
|
|
34
32
|
hold_id - Unique ID for this button instance (default: "hold")
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
string and the hold-button:validate event (default: 750)
|
|
40
|
-
early_action - JS expression executed early (for Web3 users) (default: none)
|
|
41
|
-
early_action_at - ms into the hold at which the early action runs: the
|
|
42
|
-
early_action string and the hold-button:early event
|
|
43
|
-
(default: 1500)
|
|
44
|
-
early_action_guard - JS expression that must be truthy for early action (default: none)
|
|
45
|
-
on_hold_start - JS expression executed the instant a hold BEGINS, after
|
|
46
|
-
the guard/submitting checks pass (default: none). Used to
|
|
47
|
-
kick off work that should run in parallel with the hold
|
|
48
|
-
(e.g. the board's authoritative funding pre-check fetch,
|
|
49
|
-
resolved by the time the hold completes ~2s later).
|
|
33
|
+
validate_at - ms into the hold at which hold-button:validate is
|
|
34
|
+
dispatched (default: 750)
|
|
35
|
+
early_action_at - ms into the hold at which hold-button:early is
|
|
36
|
+
dispatched (default: 1500)
|
|
50
37
|
fizz - Render the fizz particle layer (default: true). Pass
|
|
51
38
|
false for a flat button. Specimens: /admin/style.
|
|
52
39
|
fizz_level - :lively (default) or :calm. Lively rests at a full
|
|
@@ -69,20 +56,15 @@
|
|
|
69
56
|
the six picked teams' light / dark / alt colors onto
|
|
70
57
|
--fizz-c-1..18, three per zone in pick order.
|
|
71
58
|
%>
|
|
59
|
+
<% Studio::HoldButton.refuse_removed_locals!(local_assigns) %>
|
|
72
60
|
<% default_text ||= "Hold to Confirm" %>
|
|
73
61
|
<% hold_text ||= "Almost there..." %>
|
|
74
62
|
<% success_text ||= "Confirmed!" %>
|
|
75
63
|
<% error_text ||= "Entry Blocked" %>
|
|
76
64
|
<% duration ||= 2000 %>
|
|
77
65
|
<% hold_id ||= "hold" %>
|
|
78
|
-
<% guard ||= nil %>
|
|
79
|
-
<% on_success ||= nil %>
|
|
80
|
-
<% validate ||= nil %>
|
|
81
66
|
<% validate_at ||= 750 %>
|
|
82
|
-
<% early_action ||= nil %>
|
|
83
67
|
<% early_action_at ||= 1500 %>
|
|
84
|
-
<% early_action_guard ||= nil %>
|
|
85
|
-
<% on_hold_start ||= nil %>
|
|
86
68
|
<%# `fizz ||= true` would coerce an explicit `fizz: false` back to true, so
|
|
87
69
|
read the key instead of the value. %>
|
|
88
70
|
<% fizz = local_assigns.key?(:fizz) ? local_assigns[:fizz] : true %>
|
|
@@ -125,12 +107,8 @@
|
|
|
125
107
|
<button class="hold-btn"
|
|
126
108
|
data-hold-id="<%= hold_id %>"
|
|
127
109
|
data-duration="<%= duration %>"
|
|
128
|
-
<% if
|
|
129
|
-
<% if
|
|
130
|
-
<% if on_success %>data-on-success="<%= on_success %>"<% end %>
|
|
131
|
-
<% if validate %>data-validate="<%= validate %>" data-validate-at="<%= validate_at %>"<% elsif local_assigns.key?(:validate_at) %>data-validate-at="<%= validate_at %>"<% end %>
|
|
132
|
-
<% if early_action %>data-early-action="<%= early_action %>" data-early-action-at="<%= early_action_at %>"<% elsif local_assigns.key?(:early_action_at) %>data-early-action-at="<%= early_action_at %>"<% end %>
|
|
133
|
-
<% if early_action_guard %>data-early-action-guard="<%= early_action_guard %>"<% end %>
|
|
110
|
+
<% if local_assigns.key?(:validate_at) %>data-validate-at="<%= validate_at %>"<% end %>
|
|
111
|
+
<% if local_assigns.key?(:early_action_at) %>data-early-action-at="<%= early_action_at %>"<% end %>
|
|
134
112
|
style="--duration: <%= duration %>ms"
|
|
135
113
|
data-studio-action="mousedown->hold-button#start mouseup->hold-button#end mouseleave->hold-button#end touchstart->hold-button#press touchend->hold-button#end touchcancel->hold-button#end">
|
|
136
114
|
<div class="hold-icon">
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Studio
|
|
4
|
+
# The locals studio/_hold_button no longer takes. Each carried JavaScript as a
|
|
5
|
+
# string for the browser to evaluate; a page answers the event instead.
|
|
6
|
+
module HoldButton
|
|
7
|
+
# Local => the event that answers now. The browser's half of this table is
|
|
8
|
+
# REMOVED_HOOKS in app/javascript/studio/hold_button_hooks.js.
|
|
9
|
+
REMOVED_LOCALS = {
|
|
10
|
+
guard: "hold-button:guard",
|
|
11
|
+
on_hold_start: "hold-button:start",
|
|
12
|
+
validate: "hold-button:validate",
|
|
13
|
+
early_action: "hold-button:early",
|
|
14
|
+
early_action_guard: "hold-button:early",
|
|
15
|
+
on_success: "hold-button:success"
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
# Raises when a caller passes a removed local, in every environment: a
|
|
21
|
+
# button rendered without the check its caller asked for must not reach a
|
|
22
|
+
# page.
|
|
23
|
+
def refuse_removed_locals!(locals)
|
|
24
|
+
passed = REMOVED_LOCALS.select { |name, _| !locals[name].nil? }
|
|
25
|
+
return if passed.empty?
|
|
26
|
+
|
|
27
|
+
named = passed.map { |name, event| "#{name}: (answer #{event})" }.join(", ")
|
|
28
|
+
raise ArgumentError, "studio/hold_button no longer evaluates JavaScript-string locals. Remove #{named}. " \
|
|
29
|
+
"The events are listed at the top of app/views/studio/_hold_button.html.erb."
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
data/lib/studio/version.rb
CHANGED
data/lib/studio.rb
CHANGED
|
@@ -26,6 +26,7 @@ require "studio/theme_resolver"
|
|
|
26
26
|
require "studio/ui_primitives"
|
|
27
27
|
require "studio/js_literal"
|
|
28
28
|
require "studio/js_identifier"
|
|
29
|
+
require "studio/hold_button"
|
|
29
30
|
require "studio/partial_block"
|
|
30
31
|
require "studio/sidebar_sections"
|
|
31
32
|
require "studio/site_footer"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: studio-engine
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.101.0.rc1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alex McRitchie
|
|
@@ -733,6 +733,7 @@ files:
|
|
|
733
733
|
- lib/studio/geo.rb
|
|
734
734
|
- lib/studio/geo/countries.rb
|
|
735
735
|
- lib/studio/geo/lookup.rb
|
|
736
|
+
- lib/studio/hold_button.rb
|
|
736
737
|
- lib/studio/host_schema.rb
|
|
737
738
|
- lib/studio/image_cache.rb
|
|
738
739
|
- lib/studio/ip_locations.rb
|