studio-engine 0.131.0 → 0.133.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 +4 -4
- data/CHANGELOG.md +96 -1
- data/README.md +65 -28
- data/app/assets/tailwind/studio_engine/engine.css +158 -2
- data/app/javascript/studio/application.js +1 -0
- data/app/javascript/studio/carousel.js +74 -1
- data/app/javascript/studio/controllers/carousel_controller.js +163 -9
- data/app/javascript/studio/controllers/page_header_controller.js +64 -1
- data/app/javascript/studio/controllers/style_section_nav_controller.js +26 -0
- data/app/javascript/studio/in_page_links.js +308 -0
- data/app/javascript/studio/page_header.js +17 -0
- data/app/views/studio/_carousel.html.erb +54 -11
- data/app/views/studio/_page_header.html.erb +64 -12
- data/app/views/studio/auth/_sign_in_body.html.erb +236 -0
- data/app/views/studio/auth/_sign_in_card.html.erb +50 -0
- data/app/views/studio/auth/_sign_in_modal.html.erb +36 -0
- data/app/views/studio/modals/auth/_resend_footer.html.erb +6 -0
- data/app/views/style/_glass.html.erb +4 -8
- data/app/views/style/_loaders.html.erb +11 -35
- data/app/views/style/_modals.html.erb +29 -39
- data/app/views/style/_scrims.html.erb +10 -20
- data/app/views/style/_site_footer.html.erb +10 -18
- data/app/views/style/_specimen.html.erb +13 -41
- data/app/views/style/_tasks.html.erb +4 -22
- data/app/views/style/_trick_label.html.erb +33 -0
- data/app/views/style/_tricks.html.erb +127 -207
- data/app/views/style/index.html.erb +2 -2
- data/app/views/style/modals/_wallet_connect.html.erb +2 -1
- data/config/studio_tricks.yml +470 -0
- data/lib/studio/page_header.rb +53 -0
- data/lib/studio/tricks.rb +110 -0
- data/lib/studio/version.rb +1 -1
- data/lib/studio.rb +6 -4
- metadata +9 -3
- data/app/views/style/modals/_ds_email_field.html.erb +0 -58
- data/app/views/style/modals/_ds_resend_footer.html.erb +0 -70
|
@@ -19,22 +19,65 @@
|
|
|
19
19
|
// slide is a stop, an arrow moves one slide, and the slide nearest the centre
|
|
20
20
|
// carries data-current, which the CSS styles where scroll-driven animations are
|
|
21
21
|
// missing and under reduced motion.
|
|
22
|
-
|
|
22
|
+
//
|
|
23
|
+
// THE START (the partial's start:, data-carousel-start): a row that opens past its
|
|
24
|
+
// first slide. The browser lands there before the first paint where it has
|
|
25
|
+
// scroll-initial-target (engine.css); elsewhere connect() jumps there, with no glide
|
|
26
|
+
// and nothing in the status line. A row already moved (the browser's own start, a
|
|
27
|
+
// restored scroll, a #fragment inside it) is left where it is.
|
|
28
|
+
//
|
|
29
|
+
// A LOOPING row (loop:, data-carousel-loop) wraps: Next at the last stop goes back
|
|
30
|
+
// to the first, Previous at the first to the last, and neither arrow is ever
|
|
31
|
+
// aria-disabled. There are no cloned slides, so a screen reader reads each once:
|
|
32
|
+
// the row glides back across itself.
|
|
33
|
+
//
|
|
34
|
+
// AUTOPLAY (autoplay: seconds, data-carousel-autoplay) moves the row one stop each
|
|
35
|
+
// interval. It holds still while the pointer is over the slides, while keyboard focus
|
|
36
|
+
// is in it, while the tab is hidden or the row is off screen, and after a swipe,
|
|
37
|
+
// wheel, arrow, dot or key until the reader has left it alone a while (idleDelay).
|
|
38
|
+
// The pause button (WCAG 2.2.2) stops it until pressed again; under reduced motion
|
|
39
|
+
// the row starts stopped. A move by autoplay writes nothing to the status line
|
|
40
|
+
// (APG: a rotating carousel does not announce). Without loop it stops at the end.
|
|
23
41
|
import { Controller } from "@hotwired/stimulus"
|
|
24
|
-
import {
|
|
42
|
+
import {
|
|
43
|
+
TOLERANCE, autoplayRunning, autoplaySeconds, autoplayStop, centerStops, currentStop, hiddenEnds, idleDelay,
|
|
44
|
+
initialReasons, keyAction, ownsKey, perPage, positionText, startStop, stops, targetStop, toggleLabel, withReason
|
|
45
|
+
} from "studio/carousel"
|
|
46
|
+
|
|
47
|
+
const REDUCE = "(prefers-reduced-motion: reduce)"
|
|
25
48
|
|
|
26
49
|
export default class extends Controller {
|
|
27
|
-
static targets = ["track", "slide", "prev", "next", "dot", "status"]
|
|
50
|
+
static targets = ["track", "slide", "prev", "next", "dot", "status", "toggle"]
|
|
28
51
|
|
|
29
52
|
connect() {
|
|
30
53
|
this.schedule = this.schedule.bind(this)
|
|
54
|
+
this.loop = this.element.hasAttribute("data-carousel-loop")
|
|
31
55
|
this.trackTarget.addEventListener("scroll", this.schedule, { passive: true })
|
|
32
56
|
if (typeof ResizeObserver === "function") {
|
|
33
57
|
this.observer = new ResizeObserver(this.schedule)
|
|
34
58
|
this.observer.observe(this.trackTarget)
|
|
35
59
|
}
|
|
36
60
|
this.element.setAttribute("data-carousel-ready", "")
|
|
61
|
+
this.openAtStart()
|
|
37
62
|
this.sync()
|
|
63
|
+
this.startAutoplay()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
openAtStart() {
|
|
67
|
+
const start = Number(this.element.dataset.carouselStart)
|
|
68
|
+
if (!(start > 0)) return
|
|
69
|
+
const track = this.trackTarget
|
|
70
|
+
if (Math.abs(track.scrollLeft) > TOLERANCE) return
|
|
71
|
+
const hash = window.location?.hash
|
|
72
|
+
if (hash && hash.length > 1) {
|
|
73
|
+
let target = null
|
|
74
|
+
try { target = document.getElementById(decodeURIComponent(hash.slice(1))) } catch { target = null }
|
|
75
|
+
if (target && track.contains(target)) return
|
|
76
|
+
}
|
|
77
|
+
const { positions } = this.measure()
|
|
78
|
+
const left = positions[startStop(start, positions.length)]
|
|
79
|
+
if (left === undefined) return
|
|
80
|
+
track.scrollTo({ left, behavior: "instant" })
|
|
38
81
|
}
|
|
39
82
|
|
|
40
83
|
disconnect() {
|
|
@@ -42,6 +85,112 @@ export default class extends Controller {
|
|
|
42
85
|
this.observer?.disconnect()
|
|
43
86
|
if (this.frame) cancelAnimationFrame(this.frame)
|
|
44
87
|
this.frame = null
|
|
88
|
+
this.stopAutoplay()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// -- autoplay ---------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
startAutoplay() {
|
|
94
|
+
this.seconds = autoplaySeconds(this.element.dataset.carouselAutoplay)
|
|
95
|
+
if (this.seconds === null) return
|
|
96
|
+
|
|
97
|
+
this.motion = window.matchMedia?.(REDUCE)
|
|
98
|
+
this.reasons = initialReasons({ reducedMotion: !!this.motion?.matches, hidden: document.hidden })
|
|
99
|
+
this.listeners = [
|
|
100
|
+
[this.trackTarget, "pointerenter", (event) => { if (event.pointerType !== "touch") this.pause("hover", true) }],
|
|
101
|
+
[this.trackTarget, "pointerleave", (event) => { if (event.pointerType !== "touch") this.pause("hover", false) }],
|
|
102
|
+
[this.element, "focusin", (event) => this.pause("focus", this.keyboardFocus(event.target))],
|
|
103
|
+
[this.element, "focusout", (event) => { if (!this.element.contains(event.relatedTarget)) this.pause("focus", false) }],
|
|
104
|
+
[this.trackTarget, "pointerdown", () => this.interacted()],
|
|
105
|
+
[this.trackTarget, "wheel", () => this.interacted()],
|
|
106
|
+
[this.trackTarget, "touchstart", () => this.interacted()],
|
|
107
|
+
[document, "visibilitychange", () => this.pause("hidden", document.hidden)],
|
|
108
|
+
[this.motion, "change", (event) => { if (event.matches) this.pause("stopped", true) }]
|
|
109
|
+
].filter(([target]) => target?.addEventListener)
|
|
110
|
+
this.listeners.forEach(([target, type, handler]) => target.addEventListener(type, handler, { passive: true }))
|
|
111
|
+
|
|
112
|
+
if (typeof IntersectionObserver === "function") {
|
|
113
|
+
this.viewport = new IntersectionObserver(([entry]) => this.pause("offscreen", !entry.isIntersecting))
|
|
114
|
+
this.viewport.observe(this.element)
|
|
115
|
+
}
|
|
116
|
+
this.refresh()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
stopAutoplay() {
|
|
120
|
+
this.listeners?.forEach(([target, type, handler]) => target.removeEventListener(type, handler))
|
|
121
|
+
this.listeners = null
|
|
122
|
+
this.viewport?.disconnect()
|
|
123
|
+
this.viewport = null
|
|
124
|
+
clearTimeout(this.timer)
|
|
125
|
+
clearTimeout(this.idle)
|
|
126
|
+
this.timer = this.idle = null
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Focus by keyboard holds the row; a mouse click that focuses an arrow does not.
|
|
130
|
+
// The pause button is the exception: a reader who tabs to it and presses Play
|
|
131
|
+
// means it to play.
|
|
132
|
+
keyboardFocus(target) {
|
|
133
|
+
if (this.hasToggleTarget && this.toggleTarget.contains(target)) return false
|
|
134
|
+
try { return target.matches(":focus-visible") } catch { return true }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
pause(reason, on) {
|
|
138
|
+
if (this.seconds === null || !this.reasons) return
|
|
139
|
+
this.reasons = withReason(this.reasons, reason, on)
|
|
140
|
+
this.refresh()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// A swipe, a wheel, an arrow, a dot or a key: hold still until the reader has
|
|
144
|
+
// left the row alone for idleDelay.
|
|
145
|
+
interacted() {
|
|
146
|
+
if (this.seconds === null || !this.reasons) return
|
|
147
|
+
clearTimeout(this.idle)
|
|
148
|
+
this.idle = setTimeout(() => { this.idle = null; this.pause("interaction", false) }, idleDelay(this.seconds))
|
|
149
|
+
this.pause("interaction", true)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// The pause button.
|
|
153
|
+
toggle() {
|
|
154
|
+
if (this.seconds === null || !this.reasons) return
|
|
155
|
+
const stopped = !this.reasons.has("stopped")
|
|
156
|
+
if (!stopped) {
|
|
157
|
+
// Play at the end of a row that does not loop starts it again from the first.
|
|
158
|
+
this.sync()
|
|
159
|
+
if (!this.loop && autoplayStop(this.current, this.positions.length, false) === null && this.current > 0) {
|
|
160
|
+
this.scrollToStop(0, { announce: false })
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
this.pause("stopped", stopped)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
refresh() {
|
|
167
|
+
const running = autoplayRunning(this.seconds, this.reasons)
|
|
168
|
+
const stopped = this.reasons.has("stopped")
|
|
169
|
+
this.element.toggleAttribute("data-carousel-playing", running)
|
|
170
|
+
if (this.hasToggleTarget) {
|
|
171
|
+
this.toggleTarget.setAttribute("aria-label", toggleLabel(stopped))
|
|
172
|
+
this.toggleTarget.toggleAttribute("data-carousel-stopped", stopped)
|
|
173
|
+
}
|
|
174
|
+
if (!running) {
|
|
175
|
+
clearTimeout(this.timer)
|
|
176
|
+
this.timer = null
|
|
177
|
+
} else if (!this.timer) {
|
|
178
|
+
this.timer = setTimeout(() => this.tick(), this.seconds * 1000)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
tick() {
|
|
183
|
+
this.timer = null
|
|
184
|
+
this.sync()
|
|
185
|
+
const count = this.positions.length
|
|
186
|
+
const to = autoplayStop(this.current, count, this.loop)
|
|
187
|
+
if (to === null && count > 1) {
|
|
188
|
+
// The end of a row that does not loop: autoplay is done until Play.
|
|
189
|
+
this.pause("stopped", true)
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
if (to !== null) this.scrollToStop(to, { announce: false })
|
|
193
|
+
this.refresh()
|
|
45
194
|
}
|
|
46
195
|
|
|
47
196
|
schedule() {
|
|
@@ -81,8 +230,10 @@ export default class extends Controller {
|
|
|
81
230
|
this.element.toggleAttribute("data-carousel-static", positions.length < 2)
|
|
82
231
|
track.toggleAttribute("data-fade-start", ends.start)
|
|
83
232
|
track.toggleAttribute("data-fade-end", ends.end)
|
|
84
|
-
|
|
85
|
-
|
|
233
|
+
// A looping row never disables an arrow: each end leads round to the other.
|
|
234
|
+
const wraps = this.loop && positions.length > 1
|
|
235
|
+
if (this.hasPrevTarget) this.prevTarget.setAttribute("aria-disabled", String(!wraps && !ends.start))
|
|
236
|
+
if (this.hasNextTarget) this.nextTarget.setAttribute("aria-disabled", String(!wraps && !ends.end))
|
|
86
237
|
if (this.element.classList.contains("carousel-spotlight")) {
|
|
87
238
|
this.slideTargets.forEach((slide, index) => slide.toggleAttribute("data-current", index === this.current))
|
|
88
239
|
}
|
|
@@ -98,6 +249,7 @@ export default class extends Controller {
|
|
|
98
249
|
next() { this.move("next") }
|
|
99
250
|
|
|
100
251
|
goTo(event) {
|
|
252
|
+
this.interacted()
|
|
101
253
|
this.sync()
|
|
102
254
|
this.scrollToStop(Number(event.currentTarget.dataset.carouselIndex))
|
|
103
255
|
}
|
|
@@ -116,20 +268,22 @@ export default class extends Controller {
|
|
|
116
268
|
// A press that goes nowhere (an arrow at its end, Home at the start) scrolls
|
|
117
269
|
// nothing and says nothing: the status line speaks only when the row moves.
|
|
118
270
|
move(action) {
|
|
271
|
+
this.interacted()
|
|
119
272
|
this.sync()
|
|
120
|
-
const to = targetStop(action, this.current, this.page, this.positions.length)
|
|
273
|
+
const to = targetStop(action, this.current, this.page, this.positions.length, this.loop)
|
|
121
274
|
if (to === this.current) return
|
|
122
275
|
this.scrollToStop(to)
|
|
123
276
|
}
|
|
124
277
|
|
|
125
|
-
|
|
278
|
+
// announce: false for autoplay, which moves the row without a word.
|
|
279
|
+
scrollToStop(index, { announce = true } = {}) {
|
|
126
280
|
const left = this.positions[index]
|
|
127
281
|
if (left === undefined) return
|
|
128
282
|
|
|
129
|
-
const reduce = window.matchMedia?.(
|
|
283
|
+
const reduce = window.matchMedia?.(REDUCE).matches
|
|
130
284
|
this.trackTarget.scrollTo({ left, behavior: reduce ? "auto" : "smooth" })
|
|
131
285
|
|
|
132
|
-
if (!this.hasStatusTarget) return
|
|
286
|
+
if (!announce || !this.hasStatusTarget) return
|
|
133
287
|
const total = this.slideTargets.length
|
|
134
288
|
const last = index === this.positions.length - 1
|
|
135
289
|
const first = last ? Math.max(0, total - this.page) : index
|
|
@@ -9,8 +9,14 @@
|
|
|
9
9
|
// row's tabs are links, so they work without it. If it fails to load, the sentinel
|
|
10
10
|
// carries data-studio-controller-failed and engine.css gives the row its bar at
|
|
11
11
|
// all times, so a stuck row never lays a transparent title over the page.
|
|
12
|
+
//
|
|
13
|
+
// It also keeps each quick-links menu's button honest: aria-expanded follows the
|
|
14
|
+
// popover, ArrowDown from the button walks into the list, and where the browser has
|
|
15
|
+
// no anchor positioning it places the list under the button. The popover itself
|
|
16
|
+
// opens, closes on Escape and gives focus back with no script, so a failed load
|
|
17
|
+
// leaves a menu that still works.
|
|
12
18
|
import { Controller } from "@hotwired/stimulus"
|
|
13
|
-
import { hiddenEnds, isStuck, revealBy, sentinelMarksThePlace } from "studio/page_header"
|
|
19
|
+
import { hiddenEnds, isStuck, menuPlacement, revealBy, sentinelMarksThePlace } from "studio/page_header"
|
|
14
20
|
|
|
15
21
|
export default class extends Controller {
|
|
16
22
|
connect() {
|
|
@@ -22,6 +28,7 @@ export default class extends Controller {
|
|
|
22
28
|
window.addEventListener("resize", this.schedule, { passive: true })
|
|
23
29
|
this.tabs?.addEventListener("scroll", this.schedule, { passive: true })
|
|
24
30
|
this.revealOpenTab()
|
|
31
|
+
this.connectMenus()
|
|
25
32
|
this.sync()
|
|
26
33
|
}
|
|
27
34
|
|
|
@@ -31,6 +38,53 @@ export default class extends Controller {
|
|
|
31
38
|
this.tabs?.removeEventListener("scroll", this.schedule)
|
|
32
39
|
if (this.frame) cancelAnimationFrame(this.frame)
|
|
33
40
|
this.frame = null
|
|
41
|
+
this.disconnectMenus()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
connectMenus() {
|
|
45
|
+
this.menus = [...this.row.querySelectorAll(".page-header-menu")].map((menu) => {
|
|
46
|
+
const button = menu.querySelector("[popovertarget]")
|
|
47
|
+
const list = menu.querySelector("[popover]")
|
|
48
|
+
if (!button || !list) return null
|
|
49
|
+
button.setAttribute("aria-expanded", "false")
|
|
50
|
+
const onToggle = (event) => this.menuToggled(button, list, event.newState === "open")
|
|
51
|
+
const onKey = (event) => {
|
|
52
|
+
if (event.key !== "ArrowDown") return
|
|
53
|
+
event.preventDefault()
|
|
54
|
+
if (!list.matches(":popover-open")) list.showPopover?.()
|
|
55
|
+
list.querySelector("a")?.focus()
|
|
56
|
+
}
|
|
57
|
+
list.addEventListener("toggle", onToggle)
|
|
58
|
+
button.addEventListener("keydown", onKey)
|
|
59
|
+
return { button, list, onToggle, onKey }
|
|
60
|
+
}).filter(Boolean)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
disconnectMenus() {
|
|
64
|
+
for (const { button, list, onToggle, onKey } of this.menus || []) {
|
|
65
|
+
list.removeEventListener("toggle", onToggle)
|
|
66
|
+
button.removeEventListener("keydown", onKey)
|
|
67
|
+
if (list.matches?.(":popover-open")) list.hidePopover?.()
|
|
68
|
+
}
|
|
69
|
+
this.menus = []
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
menuToggled(button, list, open) {
|
|
73
|
+
button.setAttribute("aria-expanded", String(open))
|
|
74
|
+
if (open) {
|
|
75
|
+
if (!anchorsSupported()) this.placeMenu(button, list)
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
// The browser gives focus back on Escape; a focus left on nothing comes back too.
|
|
79
|
+
if (document.activeElement === document.body || list.contains(document.activeElement)) button.focus()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
placeMenu(button, list) {
|
|
83
|
+
const place = menuPlacement(button.getBoundingClientRect(), list.getBoundingClientRect(),
|
|
84
|
+
{ width: window.innerWidth, height: window.innerHeight })
|
|
85
|
+
list.style.top = `${place.top}px`
|
|
86
|
+
list.style.left = `${place.left}px`
|
|
87
|
+
list.style.right = "auto"
|
|
34
88
|
}
|
|
35
89
|
|
|
36
90
|
schedule() {
|
|
@@ -46,6 +100,11 @@ export default class extends Controller {
|
|
|
46
100
|
const ends = this.tabs ? hiddenEnds(this.tabs) : null
|
|
47
101
|
|
|
48
102
|
this.row.toggleAttribute("data-stuck", stuck)
|
|
103
|
+
if (!anchorsSupported()) {
|
|
104
|
+
for (const { button, list } of this.menus || []) {
|
|
105
|
+
if (list.matches(":popover-open")) this.placeMenu(button, list)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
49
108
|
if (!ends) return
|
|
50
109
|
this.tabs.toggleAttribute("data-fade-start", ends.start)
|
|
51
110
|
this.tabs.toggleAttribute("data-fade-end", ends.end)
|
|
@@ -72,3 +131,7 @@ export default class extends Controller {
|
|
|
72
131
|
this.tabs.scrollLeft += revealBy(this.tabs.getBoundingClientRect(), open.getBoundingClientRect())
|
|
73
132
|
}
|
|
74
133
|
}
|
|
134
|
+
|
|
135
|
+
function anchorsSupported() {
|
|
136
|
+
return typeof CSS !== "undefined" && CSS.supports?.("anchor-name: --a") && CSS.supports("position-anchor: --a")
|
|
137
|
+
}
|
|
@@ -6,8 +6,13 @@
|
|
|
6
6
|
// <a href="#theme" data-style-section-nav-target="link">Theme</a>
|
|
7
7
|
//
|
|
8
8
|
// Registered by studio/style_guide, which only the style guide loads.
|
|
9
|
+
//
|
|
10
|
+
// A tab's click is studio/in_page_links': a smooth scroll down the page. The clicked tab
|
|
11
|
+
// holds the highlight until that scroll settles, so the row does not run through every tab
|
|
12
|
+
// the scroll passes on its way.
|
|
9
13
|
import { Controller } from "@hotwired/stimulus"
|
|
10
14
|
import { activeIndex } from "studio/style_section_nav"
|
|
15
|
+
import { SCROLL_EVENT, SCROLL_END_EVENT } from "studio/in_page_links"
|
|
11
16
|
|
|
12
17
|
const ACTIVE = "btn-primary"
|
|
13
18
|
const REST = "btn-neutral"
|
|
@@ -22,14 +27,21 @@ export default class extends Controller {
|
|
|
22
27
|
|
|
23
28
|
connect() {
|
|
24
29
|
this.schedule = this.schedule.bind(this)
|
|
30
|
+
this.hold = this.hold.bind(this)
|
|
31
|
+
this.release = this.release.bind(this)
|
|
25
32
|
window.addEventListener("scroll", this.schedule, { passive: true })
|
|
26
33
|
window.addEventListener("resize", this.schedule)
|
|
34
|
+
this.element.addEventListener(SCROLL_EVENT, this.hold)
|
|
35
|
+
document.addEventListener(SCROLL_END_EVENT, this.release)
|
|
27
36
|
this.sync()
|
|
28
37
|
}
|
|
29
38
|
|
|
30
39
|
disconnect() {
|
|
31
40
|
window.removeEventListener("scroll", this.schedule)
|
|
32
41
|
window.removeEventListener("resize", this.schedule)
|
|
42
|
+
this.element.removeEventListener(SCROLL_EVENT, this.hold)
|
|
43
|
+
document.removeEventListener(SCROLL_END_EVENT, this.release)
|
|
44
|
+
this.held = null
|
|
33
45
|
if (this.frame) cancelAnimationFrame(this.frame)
|
|
34
46
|
this.frame = null
|
|
35
47
|
}
|
|
@@ -42,7 +54,21 @@ export default class extends Controller {
|
|
|
42
54
|
})
|
|
43
55
|
}
|
|
44
56
|
|
|
57
|
+
hold(event) {
|
|
58
|
+
const link = event.detail && event.detail.link
|
|
59
|
+
if (!this.linkTargets.includes(link)) return
|
|
60
|
+
this.held = link
|
|
61
|
+
this.highlight(link)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
release() {
|
|
65
|
+
if (!this.held) return
|
|
66
|
+
this.held = null
|
|
67
|
+
this.sync()
|
|
68
|
+
}
|
|
69
|
+
|
|
45
70
|
sync() {
|
|
71
|
+
if (this.held) return
|
|
46
72
|
const links = this.linkTargets
|
|
47
73
|
const tops = links.map((link) => {
|
|
48
74
|
const section = document.getElementById(link.getAttribute("href").slice(1))
|