rytm-webflow 2.3.0 → 2.3.2
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.
- package/.claude/settings.local.json +10 -2
- package/.mcp.json +8 -0
- package/CLAUDE.md +2 -1
- package/package.json +1 -1
- package/scripts/aswap/WebflowListView.js +182 -2
- package/scripts/aswap/WebflowView.js +47 -3
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"permissions": {
|
|
3
3
|
"allow": [
|
|
4
|
-
"Bash(git fetch:*)"
|
|
4
|
+
"Bash(git fetch:*)",
|
|
5
|
+
"mcp__metrum__get_task",
|
|
6
|
+
"Bash(git checkout:*)",
|
|
7
|
+
"mcp__metrum__set_subtask_status",
|
|
8
|
+
"Bash(npm run:*)"
|
|
5
9
|
]
|
|
6
|
-
}
|
|
10
|
+
},
|
|
11
|
+
"enableAllProjectMcpServers": true,
|
|
12
|
+
"enabledMcpjsonServers": [
|
|
13
|
+
"metrum"
|
|
14
|
+
]
|
|
7
15
|
}
|
package/.mcp.json
ADDED
package/CLAUDE.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
AJAX view-swapping engine and scroll-triggered animation framework for tuki CMS projects.
|
|
4
4
|
Built on GSAP + ScrollMagic. Transforms server-rendered pages into pseudo-SPAs.
|
|
5
5
|
|
|
6
|
-
**Version**: 2.2
|
|
6
|
+
**Version**: 2.3.2 | **Entry**: `scripts/index.js` | **Deps**: gsap, imagesloaded, scrollmagic
|
|
7
7
|
|
|
8
8
|
## Exports
|
|
9
9
|
|
|
@@ -361,3 +361,4 @@ RytmWebflow.scrollController.destroy() // Clean up
|
|
|
361
361
|
7. **Stagger not working** — requires `data-webset="selector:..."` attribute
|
|
362
362
|
8. **Easing not applied** — use GSAP v3 syntax: `e:power3.out` (not `e:Power3.easeOut`)
|
|
363
363
|
9. **Cache causes stale content** — add `no-as-cache` class to forms or dynamic links
|
|
364
|
+
10. **Webscroll elements and viewport growth** — since 2.3.2 `webscroll` elements that enter the viewport via a resize / devtools toggle / background-tab activation (no scroll) are revealed automatically; earlier versions only fired on a `FORWARD` scroll crossing
|
package/package.json
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import ScrollMagic from 'scrollmagic';
|
|
1
2
|
import gsap from 'gsap';
|
|
2
3
|
import View from './View';
|
|
4
|
+
import scrollController from './../showup/ScrollController';
|
|
3
5
|
import { getWebflowAnimationProps, parseProps } from './../lib/dataTweenParser';
|
|
6
|
+
import { getScrollMagicSceneProps } from './../lib/dataScrollMagicParser';
|
|
7
|
+
import { elementIsVisibleInViewport } from '../lib/helpers';
|
|
4
8
|
import { CLASS_NAME_WEBSCROLL_FIRED } from './WebflowView';
|
|
5
9
|
|
|
6
10
|
// data-webscroll-... (scroll magic)
|
|
@@ -14,6 +18,8 @@ class WebflowListView extends View {
|
|
|
14
18
|
|
|
15
19
|
constructor(id) {
|
|
16
20
|
super(id);
|
|
21
|
+
this.scenes = [];
|
|
22
|
+
this._revealTimer = null;
|
|
17
23
|
}
|
|
18
24
|
/**
|
|
19
25
|
* animate in (show)
|
|
@@ -25,8 +31,15 @@ class WebflowListView extends View {
|
|
|
25
31
|
console.warn("Unknown selector for WebflowListView", this);
|
|
26
32
|
return;
|
|
27
33
|
}
|
|
28
|
-
const
|
|
29
|
-
|
|
34
|
+
const items = [...container.querySelectorAll(this.webset.selector)].filter(this.elementBelongsToView);
|
|
35
|
+
// 1. Set initial state for ALL scroll elements (regardless of viewport)
|
|
36
|
+
this.hideAllScrollElements(items);
|
|
37
|
+
// 2. Stagger-animate in-viewport items (existing behavior)
|
|
38
|
+
items.forEach(this.listElementShow.bind(this));
|
|
39
|
+
// 3. Build ScrollMagic scenes for off-screen items
|
|
40
|
+
this.buildScrollScenesForOffscreenItems(items);
|
|
41
|
+
// 4. Listen for resize to refresh ScrollMagic
|
|
42
|
+
this.addEventListeners();
|
|
30
43
|
}
|
|
31
44
|
/**
|
|
32
45
|
* animate out (hide)
|
|
@@ -40,6 +53,16 @@ class WebflowListView extends View {
|
|
|
40
53
|
}
|
|
41
54
|
const list = [...container.querySelectorAll(this.webset.selector)].filter(this.elementBelongsToView);
|
|
42
55
|
list.forEach(this.listElementHide.bind(this));
|
|
56
|
+
// Also hide off-screen scroll elements that have ScrollMagic scenes
|
|
57
|
+
this.hideOffscreenScrollElements(container);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* hidden (cleanup after hide animation)
|
|
61
|
+
**/
|
|
62
|
+
hidden(container) {
|
|
63
|
+
super.hidden(container);
|
|
64
|
+
this.removeEventListeners();
|
|
65
|
+
this.destroyScenes();
|
|
43
66
|
}
|
|
44
67
|
/**
|
|
45
68
|
* ############
|
|
@@ -106,6 +129,163 @@ class WebflowListView extends View {
|
|
|
106
129
|
});
|
|
107
130
|
}
|
|
108
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* ##################
|
|
134
|
+
* ### DOM events ###
|
|
135
|
+
* ##################
|
|
136
|
+
*/
|
|
137
|
+
handleEvent(e) {
|
|
138
|
+
switch (e.type) {
|
|
139
|
+
case 'resize':
|
|
140
|
+
case 'pageshow':
|
|
141
|
+
case 'DOMContentLoaded':
|
|
142
|
+
this.onWindowUpdate();
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
addEventListeners() {
|
|
147
|
+
window.addEventListener('resize', this);
|
|
148
|
+
window.addEventListener('pageshow', this);
|
|
149
|
+
document.addEventListener('DOMContentLoaded', this);
|
|
150
|
+
}
|
|
151
|
+
removeEventListeners() {
|
|
152
|
+
window.removeEventListener('resize', this);
|
|
153
|
+
window.removeEventListener('pageshow', this);
|
|
154
|
+
document.removeEventListener('DOMContentLoaded', this);
|
|
155
|
+
clearTimeout(this._revealTimer);
|
|
156
|
+
}
|
|
157
|
+
onWindowUpdate() {
|
|
158
|
+
if (this.scenes.length > 0) {
|
|
159
|
+
scrollController.refresh();
|
|
160
|
+
// viewport size changed - recompute viewport-dependent scene offsets
|
|
161
|
+
// (they are baked from window.innerHeight at scene-build time)
|
|
162
|
+
this.refreshSceneOffsets();
|
|
163
|
+
// reveal elements that entered the viewport without a scroll (debounced -
|
|
164
|
+
// resize fires in bursts while dragging / toggling devtools)
|
|
165
|
+
clearTimeout(this._revealTimer);
|
|
166
|
+
this._revealTimer = setTimeout(() => this.revealVisibleScrollElements(), 150);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Recompute viewport-dependent scene offsets after a viewport change
|
|
171
|
+
*/
|
|
172
|
+
refreshSceneOffsets() {
|
|
173
|
+
this.scenes.forEach(({ scene, el, setup }) => {
|
|
174
|
+
scene.offset(getScrollMagicSceneProps(el, setup).offset);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Reveal scene elements that entered the viewport without a scroll
|
|
179
|
+
* (devtools toggle, window resize, background-tab activation) -
|
|
180
|
+
* resize-driven scene updates carry scrollDirection "PAUSED", so the
|
|
181
|
+
* FORWARD guard in the scene start handler never fires for them
|
|
182
|
+
*/
|
|
183
|
+
revealVisibleScrollElements() {
|
|
184
|
+
this.scenes.forEach(({ el, propsShow }) => {
|
|
185
|
+
if (el.classList.contains(CLASS_NAME_WEBSCROLL_FIRED)) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (!elementIsVisibleInViewport(el, true)) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
gsap.to(el, {
|
|
192
|
+
duration: propsShow.time,
|
|
193
|
+
...propsShow.tween,
|
|
194
|
+
onComplete: () => {
|
|
195
|
+
el.classList.add(CLASS_NAME_WEBSCROLL_FIRED);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* #####################
|
|
202
|
+
* ### SCROLL SCENES ###
|
|
203
|
+
* #####################
|
|
204
|
+
*/
|
|
205
|
+
// set initial state for all scroll elements in list items
|
|
206
|
+
hideAllScrollElements(items) {
|
|
207
|
+
items.forEach((el) => {
|
|
208
|
+
const scrollEls = [...el.querySelectorAll('*[data-' + DATA_ATTR_WEBSCROLL_SHOW + ']')]
|
|
209
|
+
.filter(this.elementBelongsToView);
|
|
210
|
+
scrollEls.forEach((scrollEl) => {
|
|
211
|
+
const propsInitial = this.getTweenProps(scrollEl, DATA_ATTR_WEBSCROLL_INIT);
|
|
212
|
+
if (propsInitial) {
|
|
213
|
+
gsap.killTweensOf(scrollEl);
|
|
214
|
+
gsap.set(scrollEl, { ...propsInitial.tween });
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
// build ScrollMagic scenes for off-screen list items
|
|
220
|
+
buildScrollScenesForOffscreenItems(items) {
|
|
221
|
+
items.forEach((el) => {
|
|
222
|
+
if (this.isElementInViewport(el)) return;
|
|
223
|
+
const scrollEls = [...el.querySelectorAll('*[data-' + DATA_ATTR_WEBSCROLL_SHOW + ']')]
|
|
224
|
+
.filter(this.elementBelongsToView);
|
|
225
|
+
scrollEls.forEach((scrollEl) => {
|
|
226
|
+
this.buildScrollmagicScene(scrollEl);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
// build a single ScrollMagic scene for a scroll element
|
|
231
|
+
buildScrollmagicScene(el) {
|
|
232
|
+
const propsInitial = this.getTweenProps(el, DATA_ATTR_WEBSCROLL_INIT);
|
|
233
|
+
const propsShow = this.getTweenProps(el, DATA_ATTR_WEBSCROLL_SHOW);
|
|
234
|
+
const setup = parseProps(el.dataset[DATA_ATTR_SETUP] || '');
|
|
235
|
+
if (propsInitial && propsShow) {
|
|
236
|
+
const smsp = getScrollMagicSceneProps(el, setup);
|
|
237
|
+
const scene = new ScrollMagic.Scene(smsp);
|
|
238
|
+
scene.on("start", (e) => {
|
|
239
|
+
if (el.classList.contains(CLASS_NAME_WEBSCROLL_FIRED)) return;
|
|
240
|
+
if (e.scrollDirection === "FORWARD") {
|
|
241
|
+
gsap.killTweensOf(el);
|
|
242
|
+
gsap.set(el, { ...propsInitial.tween });
|
|
243
|
+
gsap.to(el, {
|
|
244
|
+
duration: propsShow.time,
|
|
245
|
+
...propsShow.tween,
|
|
246
|
+
onComplete: () => {
|
|
247
|
+
el.classList.add(CLASS_NAME_WEBSCROLL_FIRED);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
scene.on("add", () => {
|
|
253
|
+
setTimeout(() => {
|
|
254
|
+
if (elementIsVisibleInViewport(el, true)) {
|
|
255
|
+
gsap.to(el, {
|
|
256
|
+
duration: propsShow.time,
|
|
257
|
+
...propsShow.tween,
|
|
258
|
+
onComplete: () => {
|
|
259
|
+
el.classList.add(CLASS_NAME_WEBSCROLL_FIRED);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}, 10);
|
|
264
|
+
});
|
|
265
|
+
scene.addTo(scrollController.get());
|
|
266
|
+
this.scenes.push({ scene, el, propsShow, setup });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// hide off-screen scroll elements during view hide
|
|
270
|
+
hideOffscreenScrollElements(container) {
|
|
271
|
+
const scrollEls = [...container.querySelectorAll('*[data-' + DATA_ATTR_WEBSCROLL_HIDE + ']')]
|
|
272
|
+
.filter(this.elementBelongsToView);
|
|
273
|
+
scrollEls.forEach((el) => {
|
|
274
|
+
if (this.isElementInViewport(el.closest(this.webset.selector))) return;
|
|
275
|
+
const propsHide = this.getTweenProps(el, DATA_ATTR_WEBSCROLL_HIDE);
|
|
276
|
+
if (propsHide) {
|
|
277
|
+
gsap.killTweensOf(el);
|
|
278
|
+
gsap.to(el, { duration: propsHide.time, ...propsHide.tween });
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
// destroy all ScrollMagic scenes
|
|
283
|
+
destroyScenes() {
|
|
284
|
+
this.scenes.forEach(({ scene }) => {
|
|
285
|
+
scene.destroy();
|
|
286
|
+
});
|
|
287
|
+
this.scenes = [];
|
|
288
|
+
}
|
|
109
289
|
/**
|
|
110
290
|
* ###############
|
|
111
291
|
* ### HELPERS ###
|
|
@@ -28,6 +28,7 @@ class WebflowView extends View {
|
|
|
28
28
|
constructor(id) {
|
|
29
29
|
super(id);
|
|
30
30
|
this.scenes = [];
|
|
31
|
+
this._revealTimer = null;
|
|
31
32
|
}
|
|
32
33
|
/**
|
|
33
34
|
* prepare (before show)
|
|
@@ -85,6 +86,9 @@ class WebflowView extends View {
|
|
|
85
86
|
case 'resize':
|
|
86
87
|
this.onWindowUpdate(e);
|
|
87
88
|
break;
|
|
89
|
+
case 'pageshow':
|
|
90
|
+
this.onWindowUpdate(e);
|
|
91
|
+
break;
|
|
88
92
|
case 'DOMContentLoaded':
|
|
89
93
|
this.onWindowUpdate(e);
|
|
90
94
|
break;
|
|
@@ -95,6 +99,7 @@ class WebflowView extends View {
|
|
|
95
99
|
*/
|
|
96
100
|
addEventListeners() {
|
|
97
101
|
window.addEventListener('resize', this);
|
|
102
|
+
window.addEventListener('pageshow', this);
|
|
98
103
|
document.addEventListener('DOMContentLoaded', this);
|
|
99
104
|
}
|
|
100
105
|
/**
|
|
@@ -102,13 +107,53 @@ class WebflowView extends View {
|
|
|
102
107
|
*/
|
|
103
108
|
removeEventListeners() {
|
|
104
109
|
window.removeEventListener('resize', this);
|
|
110
|
+
window.removeEventListener('pageshow', this);
|
|
105
111
|
document.removeEventListener('DOMContentLoaded', this);
|
|
112
|
+
clearTimeout(this._revealTimer);
|
|
106
113
|
}
|
|
107
114
|
onWindowUpdate(e) {
|
|
108
115
|
if (this.scenes.length > 0) {
|
|
109
116
|
scrollController.refresh();
|
|
117
|
+
// viewport size changed - recompute viewport-dependent scene offsets
|
|
118
|
+
// (they are baked from window.innerHeight at scene-build time)
|
|
119
|
+
this.refreshSceneOffsets();
|
|
120
|
+
// reveal elements that entered the viewport without a scroll (debounced -
|
|
121
|
+
// resize fires in bursts while dragging / toggling devtools)
|
|
122
|
+
clearTimeout(this._revealTimer);
|
|
123
|
+
this._revealTimer = setTimeout(() => this.revealVisibleScrollElements(), 150);
|
|
110
124
|
}
|
|
111
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* Recompute viewport-dependent scene offsets after a viewport change
|
|
128
|
+
*/
|
|
129
|
+
refreshSceneOffsets() {
|
|
130
|
+
this.scenes.forEach(({ scene, el, setup }) => {
|
|
131
|
+
scene.offset(getScrollMagicSceneProps(el, setup).offset);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Reveal scene elements that entered the viewport without a scroll
|
|
136
|
+
* (devtools toggle, window resize, background-tab activation) -
|
|
137
|
+
* resize-driven scene updates carry scrollDirection "PAUSED", so the
|
|
138
|
+
* FORWARD guard in the scene start handler never fires for them
|
|
139
|
+
*/
|
|
140
|
+
revealVisibleScrollElements() {
|
|
141
|
+
this.scenes.forEach(({ el, propsShow }) => {
|
|
142
|
+
if (el.classList.contains(CLASS_NAME_WEBSCROLL_FIRED)) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (!elementIsVisibleInViewport(el, true)) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
gsap.to(el, {
|
|
149
|
+
duration: propsShow.time,
|
|
150
|
+
...propsShow.tween,
|
|
151
|
+
onComplete: () => {
|
|
152
|
+
el.classList.add(CLASS_NAME_WEBSCROLL_FIRED);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
}
|
|
112
157
|
/**
|
|
113
158
|
* ############
|
|
114
159
|
* ### SHOW ###
|
|
@@ -234,7 +279,7 @@ class WebflowView extends View {
|
|
|
234
279
|
}, 10);
|
|
235
280
|
});
|
|
236
281
|
scene.addTo(scrollController.get());
|
|
237
|
-
this.scenes.push(scene);
|
|
282
|
+
this.scenes.push({ scene, el, propsShow, setup });
|
|
238
283
|
}
|
|
239
284
|
}
|
|
240
285
|
/**
|
|
@@ -310,9 +355,8 @@ class WebflowView extends View {
|
|
|
310
355
|
}
|
|
311
356
|
// ## Destroy scrollmagic scenes
|
|
312
357
|
destroyScenes() {
|
|
313
|
-
this.scenes.forEach((scene) => {
|
|
358
|
+
this.scenes.forEach(({ scene }) => {
|
|
314
359
|
scene.destroy();
|
|
315
|
-
scene = null;
|
|
316
360
|
})
|
|
317
361
|
this.scenes = [];
|
|
318
362
|
}
|