Setting the file. One moment. Interaction Runtime · Wix Headless Replatform · wix/skills · Skills Docs28.10
Workflow
153
function requireRoot
— line 153
This file
- Number
- 28.50
- Position
- 50 of 89
- Type
- JavaScript
- Size
- 8 KB
- Lines
- 194
scripts/lib/interaction-runtime.mjs
JavaScript·194 lines·8 KB
7
const
controls
=
Array.
from
(root.
querySelectorAll
(
"[data-rp-direction]"
));
8 const track = root.querySelector("[data-rp-track]");
9 const viewport = root.querySelector("[data-rp-viewport]") || track?.parentElement || null;
10 const listeners = [];
11 const activate = (item) => {
12 if (!item || !items.includes(item)) return;
13 for (const candidate of items) {
14 const active = candidate === item;
15 candidate.setAttribute("data-rp-active", String(active));
16 candidate.setAttribute("aria-selected", String(active));
17 }
18 if (alignActive) alignItemInViewport(item, viewport);
19 root.dispatchEvent(new CustomEvent("rp:statechange", { detail: { index: items.indexOf(item) } }));
20 };
21 for (const item of items) {
22 const target = item.querySelector("[data-rp-activate]") || item;
23 listen(listeners, item, "click", (event) => {
24 const interactive = event.target?.closest?.("a,button,input,select,textarea,[role='button']");
25 if (interactive && interactive !== item && interactive !== target && !target.contains(interactive)) return;
26 activate(item);
27 });
28 listen(listeners, target, "keydown", (event) => {
29 if (event.key === "Enter" || event.key === " ") {
30 event.preventDefault();
31 activate(item);
32 }
33 });
34 if (activateOnHover) listen(listeners, item, "pointerenter", () => activate(item));
35 }
36 for (const control of controls) {
37 listen(listeners, control, "click", () => {
38 const direction = Number(control.getAttribute("data-rp-direction")) || 0;
39 const activeIndex = Math.max(0, items.findIndex((item) => item.getAttribute("data-rp-active") === "true"));
40 activate(items[wrapIndex(activeIndex + direction, items.length)]);
41 });
42 }
43 const initialState = root.getAttribute("data-rp-initial-state") || "single-active";
44 if (initialState !== "all-collapsed" && !items.some((item) => item.getAttribute("data-rp-active") === "true") && items[0]) activate(items[0]);
45 markInitialized(root, "active-card-rail");
46 return { activate, destroy: () => removeListeners(listeners) };
47}
48
49export function bindContentSwitcher(root) {
50 requireRoot(root, "content-switcher");
51 const items = Array.from(root.querySelectorAll("[data-rp-item][data-rp-state]"));
52 const panels = Array.from(root.querySelectorAll("[data-rp-panel][data-rp-state]"));
53 const listeners = [];
54 const activate = (state) => {
55 for (const item of items) {
56 const active = item.getAttribute("data-rp-state") === state;
57 item.setAttribute("data-rp-active", String(active));
58 item.setAttribute("aria-selected", String(active));
59 }
60 for (const panel of panels) {
61 const active = panel.getAttribute("data-rp-state") === state;
62 panel.hidden = !active;
63 panel.setAttribute("aria-hidden", String(!active));
64 }
65 root.dispatchEvent(new CustomEvent("rp:statechange", { detail: { state } }));
66 };
67 for (const item of items) {
68 const target = item.querySelector("[data-rp-activate]") || item;
69 listen(listeners, item, "click", () => activate(item.getAttribute("data-rp-state")));
70 }
71 const initial = items.find((item) => item.getAttribute("data-rp-active") === "true")?.getAttribute("data-rp-state") || items[0]?.getAttribute("data-rp-state");
72 if (initial) activate(initial);
73 markInitialized(root, "content-switcher");
74 return { activate, destroy: () => removeListeners(listeners) };
75}
76
77export function bindScrollScene(root, { phases = defaultScrollPhases() } = {}) {
78 requireRoot(root, "scroll-scene");
79 const ordered = normalizePhases(phases);
80 let frame = 0;
81 const update = () => {
82 frame = 0;
83 const rect = root.getBoundingClientRect();
84 const travel = Math.max(1, rect.height - window.innerHeight);
85 const progress = clamp((-rect.top) / travel, 0, 1);
86 const entryProgress = clamp((window.innerHeight - rect.top) / Math.max(1, window.innerHeight), 0, 1);
87 const phase = phaseForProgress(progress, ordered);
88 root.setAttribute("data-rp-phase", phase.name);
89 root.style.setProperty("--rp-scroll-progress", String(progress));
90 root.style.setProperty("--rp-entry-progress", String(entryProgress));
91 root.dispatchEvent(new CustomEvent("rp:scrollphase", { detail: { phase: phase.name, progress, entryProgress } }));
92 };
93 const onScroll = () => {
94 if (!frame) frame = requestAnimationFrame(update);
95 };
96 window.addEventListener("scroll", onScroll, { passive: true });
97 window.addEventListener("resize", onScroll);
98 update();
99 markInitialized(root, "scroll-scene");
100 return {
101 update,
102 destroy: () => {
103 window.removeEventListener("scroll", onScroll);
104 window.removeEventListener("resize", onScroll);
105 if (frame) cancelAnimationFrame(frame);
106 },
107 };
108}
109
110export function autoBindInteractions(scope = document) {
111 const roots = Array.from(scope.querySelectorAll("[data-rp-scene]"));
112 const bindings = [];
113 for (const root of roots) {
114 if (root.querySelector("[data-rp-track]") && !isInitialized(root, "active-card-rail")) {
115 bindings.push(bindActiveItemRail(root, {
116 activateOnHover: root.getAttribute("data-rp-activate-on-hover") === "true",
117 alignActive: root.getAttribute("data-rp-align-active") !== "false",
118 }));
119 }
120 if (root.querySelector("[data-rp-item][data-rp-state]") && root.querySelector("[data-rp-panel][data-rp-state]") && !isInitialized(root, "content-switcher")) {
121 bindings.push(bindContentSwitcher(root));
122 }
123 if (root.querySelector("[data-rp-visual]") && !isInitialized(root, "scroll-scene")) {
124 bindings.push(bindScrollScene(root));
125 }
126 }
127 return { bindings, destroy: () => bindings.forEach((binding) => binding?.destroy?.()) };
128}
129
130export function phaseForProgress(progress, phases = defaultScrollPhases()) {
131 const ordered = normalizePhases(phases);
132 const value = clamp(Number(progress) || 0, 0, 1);
133 return [...ordered].reverse().find((phase) => value >= phase.at) || ordered[0];
134}
135
136function defaultScrollPhases() {
137 return [
138 { name: "approach", at: 0 },
139 { name: "active", at: 0.25 },
140 { name: "reveal", at: 0.65 },
141 { name: "release", at: 0.92 },
142 ];
143}
144
145function normalizePhases(phases) {
146 const normalized = (phases || []).map((phase, index) => ({
147 name: String(phase.name || phase.id || `phase-${index + 1}`),
148 at: clamp(Number(phase.at ?? phase.progress ?? 0), 0, 1),
149 })).sort((left, right) => left.at - right.at);
150 return normalized.length ? normalized : defaultScrollPhases();
151}
152
153function requireRoot(root, primitive) {
154 if (!root || typeof root.querySelectorAll !== "function") throw new Error(`Missing root element for ${primitive}.`);
155}
156
157function listen(listeners, target, type, handler) {
158 target.addEventListener(type, handler);
159 listeners.push([target, type, handler]);
160}
161
162function removeListeners(listeners) {
163 for (const [target, type, handler] of listeners) target.removeEventListener(type, handler);
164}
165
166function alignItemInViewport(item, viewport) {
167 if (!viewport || typeof viewport.scrollTo !== "function") return;
168 const viewportRect = viewport.getBoundingClientRect();
169 const itemRect = item.getBoundingClientRect();
170 const left = viewport.scrollLeft + itemRect.left - viewportRect.left - ((viewportRect.width - itemRect.width) / 2);
171 viewport.scrollTo({ left: Math.max(0, left), behavior: "smooth" });
172}
173
174function initializedTokens(root) {
175 return new Set(String(root.getAttribute("data-rp-initialized") || "").split(/\s+/).filter(Boolean));
176}
177
178function isInitialized(root, primitive) {
179 return initializedTokens(root).has(primitive);
180}
181
182function markInitialized(root, primitive) {
183 const tokens = initializedTokens(root);
184 tokens.add(primitive);
185 root.setAttribute("data-rp-initialized", Array.from(tokens).join(" "));
186}
187
188function wrapIndex(index, length) {
189 return length ? (index % length + length) % length : 0;
190}
191
192function clamp(value, minimum, maximum) {
193 return Math.min(maximum, Math.max(minimum, value));
194}