rune-scroller 0.1.11 → 2.0.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.
- package/README.md +195 -29
- package/dist/__mocks__/IntersectionObserver.d.ts +25 -0
- package/dist/__mocks__/IntersectionObserver.js +116 -0
- package/dist/__mocks__/svelte-runes.d.ts +25 -0
- package/dist/__mocks__/svelte-runes.js +117 -0
- package/dist/__test-helpers__/dom.d.ts +118 -0
- package/dist/__test-helpers__/dom.js +305 -0
- package/dist/animate.d.ts +4 -0
- package/dist/animate.js +152 -0
- package/dist/animate.test.js +370 -0
- package/dist/animations.comprehensive.test.d.ts +1 -0
- package/dist/animations.comprehensive.test.js +432 -0
- package/dist/animations.css +21 -12
- package/dist/animations.d.ts +12 -9
- package/dist/animations.js +31 -6
- package/dist/animations.test.js +23 -41
- package/dist/dom-utils.d.ts +40 -0
- package/dist/dom-utils.js +111 -0
- package/dist/dom-utils.test.d.ts +1 -0
- package/dist/dom-utils.test.js +220 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.js +17 -4
- package/dist/observer-utils.d.ts +40 -0
- package/dist/observer-utils.js +50 -0
- package/dist/robustness.test.d.ts +1 -0
- package/dist/robustness.test.js +317 -0
- package/dist/runeScroller.d.ts +25 -0
- package/dist/runeScroller.integration.test.d.ts +1 -0
- package/dist/runeScroller.integration.test.js +419 -0
- package/dist/runeScroller.js +183 -0
- package/dist/runeScroller.test.d.ts +1 -0
- package/dist/runeScroller.test.js +375 -0
- package/dist/types.d.ts +104 -24
- package/dist/types.js +58 -0
- package/dist/useIntersection.svelte.d.ts +7 -12
- package/dist/useIntersection.svelte.js +75 -54
- package/dist/useIntersection.test.d.ts +1 -0
- package/dist/useIntersection.test.js +98 -0
- package/package.json +19 -18
- package/dist/BaseAnimated.svelte +0 -48
- package/dist/BaseAnimated.svelte.d.ts +0 -16
- package/dist/RuneScroller.svelte +0 -37
- package/dist/RuneScroller.svelte.d.ts +0 -16
- package/dist/animate.svelte.d.ts +0 -14
- package/dist/animate.svelte.js +0 -79
- package/dist/dom-utils.svelte.d.ts +0 -22
- package/dist/dom-utils.svelte.js +0 -46
- package/dist/runeScroller.svelte.d.ts +0 -24
- package/dist/runeScroller.svelte.js +0 -79
- package/dist/scroll-animate.test.js +0 -57
- /package/dist/{scroll-animate.test.d.ts → animate.test.d.ts} +0 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM Test Helpers
|
|
3
|
+
* Utilities for creating test elements, verifying animations, and measuring performance
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Create a test element with default styles
|
|
7
|
+
* @param {Object} options - Configuration options
|
|
8
|
+
* @param {string} options.width - Width (default: '100px')
|
|
9
|
+
* @param {string} options.height - Height (default: '100px')
|
|
10
|
+
* @param {string} options.id - Element ID
|
|
11
|
+
* @param {string} options.className - Additional CSS classes
|
|
12
|
+
* @param {Document} options.document - Document to use (for testing)
|
|
13
|
+
* @returns {HTMLElement}
|
|
14
|
+
*/
|
|
15
|
+
export function createTestElement(options?: {
|
|
16
|
+
width: string;
|
|
17
|
+
height: string;
|
|
18
|
+
id: string;
|
|
19
|
+
className: string;
|
|
20
|
+
document: Document;
|
|
21
|
+
}): HTMLElement;
|
|
22
|
+
/**
|
|
23
|
+
* Create multiple test elements
|
|
24
|
+
* @param {number} count - Number of elements to create
|
|
25
|
+
* @param {Object} options - Element options (passed to createTestElement)
|
|
26
|
+
* @returns {HTMLElement[]}
|
|
27
|
+
*/
|
|
28
|
+
export function createTestElements(count: number, options?: Object): HTMLElement[];
|
|
29
|
+
/**
|
|
30
|
+
* Get sentinel element from animated element's wrapper
|
|
31
|
+
* @param {HTMLElement} element - The animated element
|
|
32
|
+
* @returns {HTMLElement|null}
|
|
33
|
+
*/
|
|
34
|
+
export function getSentinel(element: HTMLElement): HTMLElement | null;
|
|
35
|
+
/**
|
|
36
|
+
* Check if element has animation applied
|
|
37
|
+
* @param {HTMLElement} element - Element to check
|
|
38
|
+
* @returns {boolean}
|
|
39
|
+
*/
|
|
40
|
+
export function hasAnimation(element: HTMLElement): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Check if animation is active (is-visible)
|
|
43
|
+
* @param {HTMLElement} element - Element to check
|
|
44
|
+
* @returns {boolean}
|
|
45
|
+
*/
|
|
46
|
+
export function isAnimating(element: HTMLElement): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Get animation type from element
|
|
49
|
+
* @param {HTMLElement} element - Element to check
|
|
50
|
+
* @returns {string|null}
|
|
51
|
+
*/
|
|
52
|
+
export function getAnimationType(element: HTMLElement): string | null;
|
|
53
|
+
/**
|
|
54
|
+
* Get CSS variable value from element
|
|
55
|
+
* @param {HTMLElement} element - Element to check
|
|
56
|
+
* @param {string} variableName - Variable name (with or without --)
|
|
57
|
+
* @returns {string|null}
|
|
58
|
+
*/
|
|
59
|
+
export function getCSSVariable(element: HTMLElement, variableName: string): string | null;
|
|
60
|
+
/**
|
|
61
|
+
* Create a mock ResizeObserver
|
|
62
|
+
* @param {Function} callback - Callback function
|
|
63
|
+
* @returns {Object}
|
|
64
|
+
*/
|
|
65
|
+
export function createMockResizeObserver(callback: Function): Object;
|
|
66
|
+
/**
|
|
67
|
+
* Setup a test environment with mocked DOM APIs
|
|
68
|
+
* @param {Object} options - Configuration
|
|
69
|
+
* @returns {Object} - Setup context with window, document, cleanup
|
|
70
|
+
*/
|
|
71
|
+
export function setupTestDOM(options?: Object): Object;
|
|
72
|
+
/**
|
|
73
|
+
* Measure animation performance
|
|
74
|
+
* @param {Function} fn - Function to measure
|
|
75
|
+
* @returns {Object} - Performance metrics
|
|
76
|
+
*/
|
|
77
|
+
export function measurePerformance(fn: Function): Object;
|
|
78
|
+
/**
|
|
79
|
+
* Create spacer element for pagination
|
|
80
|
+
* @param {number} height - Height in pixels
|
|
81
|
+
* @param {Document} doc - Document reference
|
|
82
|
+
* @returns {HTMLElement}
|
|
83
|
+
*/
|
|
84
|
+
export function createSpacer(height?: number, doc?: Document): HTMLElement;
|
|
85
|
+
/**
|
|
86
|
+
* Add element to DOM
|
|
87
|
+
* @param {HTMLElement} element - Element to add
|
|
88
|
+
* @param {HTMLElement} parent - Parent element (default: body)
|
|
89
|
+
*/
|
|
90
|
+
export function appendElement(element: HTMLElement, parent?: HTMLElement): void;
|
|
91
|
+
/**
|
|
92
|
+
* Remove element from DOM
|
|
93
|
+
* @param {HTMLElement} element - Element to remove
|
|
94
|
+
*/
|
|
95
|
+
export function removeElement(element: HTMLElement): void;
|
|
96
|
+
/**
|
|
97
|
+
* Clone element for comparison
|
|
98
|
+
* @param {HTMLElement} element - Element to clone
|
|
99
|
+
* @returns {Object} - Element state snapshot
|
|
100
|
+
*/
|
|
101
|
+
export function snapshotElement(element: HTMLElement): Object;
|
|
102
|
+
declare namespace _default {
|
|
103
|
+
export { createTestElement };
|
|
104
|
+
export { createTestElements };
|
|
105
|
+
export { getSentinel };
|
|
106
|
+
export { hasAnimation };
|
|
107
|
+
export { isAnimating };
|
|
108
|
+
export { getAnimationType };
|
|
109
|
+
export { getCSSVariable };
|
|
110
|
+
export { createMockResizeObserver };
|
|
111
|
+
export { setupTestDOM };
|
|
112
|
+
export { measurePerformance };
|
|
113
|
+
export { createSpacer };
|
|
114
|
+
export { appendElement };
|
|
115
|
+
export { removeElement };
|
|
116
|
+
export { snapshotElement };
|
|
117
|
+
}
|
|
118
|
+
export default _default;
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM Test Helpers
|
|
3
|
+
* Utilities for creating test elements, verifying animations, and measuring performance
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create a test element with default styles
|
|
8
|
+
* @param {Object} options - Configuration options
|
|
9
|
+
* @param {string} options.width - Width (default: '100px')
|
|
10
|
+
* @param {string} options.height - Height (default: '100px')
|
|
11
|
+
* @param {string} options.id - Element ID
|
|
12
|
+
* @param {string} options.className - Additional CSS classes
|
|
13
|
+
* @param {Document} options.document - Document to use (for testing)
|
|
14
|
+
* @returns {HTMLElement}
|
|
15
|
+
*/
|
|
16
|
+
export function createTestElement(options = {}) {
|
|
17
|
+
const {
|
|
18
|
+
width = '100px',
|
|
19
|
+
height = '100px',
|
|
20
|
+
id = null,
|
|
21
|
+
className = '',
|
|
22
|
+
document: doc = typeof document !== 'undefined' ? document : global.document
|
|
23
|
+
} = options;
|
|
24
|
+
|
|
25
|
+
const element = doc.createElement('div');
|
|
26
|
+
element.style.cssText = `
|
|
27
|
+
width: ${width};
|
|
28
|
+
height: ${height};
|
|
29
|
+
background: #ccc;
|
|
30
|
+
position: relative;
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
if (id) element.id = id;
|
|
34
|
+
if (className) element.className = className;
|
|
35
|
+
|
|
36
|
+
element.textContent = `Test ${id || 'Element'}`;
|
|
37
|
+
|
|
38
|
+
// Mock getBoundingClientRect
|
|
39
|
+
if (!element.getBoundingClientRect) {
|
|
40
|
+
element.getBoundingClientRect = () => ({
|
|
41
|
+
width: parseInt(width),
|
|
42
|
+
height: parseInt(height),
|
|
43
|
+
top: 0,
|
|
44
|
+
left: 0,
|
|
45
|
+
bottom: parseInt(height),
|
|
46
|
+
right: parseInt(width),
|
|
47
|
+
x: 0,
|
|
48
|
+
y: 0,
|
|
49
|
+
toJSON: () => ({})
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Mock offsetHeight
|
|
54
|
+
Object.defineProperty(element, 'offsetHeight', {
|
|
55
|
+
configurable: true,
|
|
56
|
+
value: parseInt(height)
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return element;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Create multiple test elements
|
|
64
|
+
* @param {number} count - Number of elements to create
|
|
65
|
+
* @param {Object} options - Element options (passed to createTestElement)
|
|
66
|
+
* @returns {HTMLElement[]}
|
|
67
|
+
*/
|
|
68
|
+
export function createTestElements(count, options = {}) {
|
|
69
|
+
const elements = [];
|
|
70
|
+
for (let i = 0; i < count; i++) {
|
|
71
|
+
elements.push(
|
|
72
|
+
createTestElement({
|
|
73
|
+
...options,
|
|
74
|
+
id: options.id ? `${options.id}-${i}` : `element-${i}`
|
|
75
|
+
})
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return elements;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get sentinel element from animated element's wrapper
|
|
83
|
+
* @param {HTMLElement} element - The animated element
|
|
84
|
+
* @returns {HTMLElement|null}
|
|
85
|
+
*/
|
|
86
|
+
export function getSentinel(element) {
|
|
87
|
+
const wrapper = element.parentElement;
|
|
88
|
+
if (!wrapper) return null;
|
|
89
|
+
|
|
90
|
+
// Sentinel is typically the last child or has data-sentinel-id attribute
|
|
91
|
+
const sentinel = wrapper.querySelector('[data-sentinel-id]');
|
|
92
|
+
if (sentinel) return sentinel;
|
|
93
|
+
|
|
94
|
+
// Fallback: find child that is not the element
|
|
95
|
+
for (let i = 0; i < wrapper.children.length; i++) {
|
|
96
|
+
if (wrapper.children[i] !== element) {
|
|
97
|
+
return wrapper.children[i];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Check if element has animation applied
|
|
106
|
+
* @param {HTMLElement} element - Element to check
|
|
107
|
+
* @returns {boolean}
|
|
108
|
+
*/
|
|
109
|
+
export function hasAnimation(element) {
|
|
110
|
+
return (
|
|
111
|
+
element.classList.contains('scroll-animate') &&
|
|
112
|
+
element.hasAttribute('data-animation')
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Check if animation is active (is-visible)
|
|
118
|
+
* @param {HTMLElement} element - Element to check
|
|
119
|
+
* @returns {boolean}
|
|
120
|
+
*/
|
|
121
|
+
export function isAnimating(element) {
|
|
122
|
+
return element.classList.contains('is-visible');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get animation type from element
|
|
127
|
+
* @param {HTMLElement} element - Element to check
|
|
128
|
+
* @returns {string|null}
|
|
129
|
+
*/
|
|
130
|
+
export function getAnimationType(element) {
|
|
131
|
+
return element.getAttribute('data-animation');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get CSS variable value from element
|
|
136
|
+
* @param {HTMLElement} element - Element to check
|
|
137
|
+
* @param {string} variableName - Variable name (with or without --)
|
|
138
|
+
* @returns {string|null}
|
|
139
|
+
*/
|
|
140
|
+
export function getCSSVariable(element, variableName) {
|
|
141
|
+
const name = variableName.startsWith('--') ? variableName : `--${variableName}`;
|
|
142
|
+
return element.style.getPropertyValue(name);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Create a mock ResizeObserver
|
|
147
|
+
* @param {Function} callback - Callback function
|
|
148
|
+
* @returns {Object}
|
|
149
|
+
*/
|
|
150
|
+
export function createMockResizeObserver(callback) {
|
|
151
|
+
const observedElements = new Set();
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
observe(element) {
|
|
155
|
+
observedElements.add(element);
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
unobserve(element) {
|
|
159
|
+
observedElements.delete(element);
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
disconnect() {
|
|
163
|
+
observedElements.clear();
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
// Testing API: trigger resize
|
|
167
|
+
trigger(element) {
|
|
168
|
+
if (observedElements.has(element)) {
|
|
169
|
+
callback([
|
|
170
|
+
{
|
|
171
|
+
target: element,
|
|
172
|
+
contentRect: element.getBoundingClientRect()
|
|
173
|
+
}
|
|
174
|
+
]);
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
triggerAll() {
|
|
179
|
+
observedElements.forEach((element) => {
|
|
180
|
+
this.trigger(element);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Setup a test environment with mocked DOM APIs
|
|
188
|
+
* @param {Object} options - Configuration
|
|
189
|
+
* @returns {Object} - Setup context with window, document, cleanup
|
|
190
|
+
*/
|
|
191
|
+
export function setupTestDOM(options = {}) {
|
|
192
|
+
const { width = 1024, height = 768 } = options;
|
|
193
|
+
|
|
194
|
+
// Already have a global document in test environment (happy-dom)
|
|
195
|
+
const doc = typeof document !== 'undefined' ? document : global.document;
|
|
196
|
+
const win = typeof window !== 'undefined' ? window : global.window;
|
|
197
|
+
|
|
198
|
+
if (doc && doc.body) {
|
|
199
|
+
doc.body.style.cssText = `width: ${width}px; height: ${height}px; margin: 0; padding: 0;`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
window: win,
|
|
204
|
+
document: doc,
|
|
205
|
+
body: doc?.body,
|
|
206
|
+
cleanup() {
|
|
207
|
+
// Clean up any test elements
|
|
208
|
+
if (doc && doc.body) {
|
|
209
|
+
doc.body.innerHTML = '';
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Measure animation performance
|
|
217
|
+
* @param {Function} fn - Function to measure
|
|
218
|
+
* @returns {Object} - Performance metrics
|
|
219
|
+
*/
|
|
220
|
+
export function measurePerformance(fn) {
|
|
221
|
+
const start = Date.now();
|
|
222
|
+
const startMemory = process.memoryUsage().heapUsed;
|
|
223
|
+
|
|
224
|
+
const result = fn();
|
|
225
|
+
|
|
226
|
+
const end = Date.now();
|
|
227
|
+
const endMemory = process.memoryUsage().heapUsed;
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
duration: end - start,
|
|
231
|
+
memory: endMemory - startMemory,
|
|
232
|
+
result
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Create spacer element for pagination
|
|
238
|
+
* @param {number} height - Height in pixels
|
|
239
|
+
* @param {Document} doc - Document reference
|
|
240
|
+
* @returns {HTMLElement}
|
|
241
|
+
*/
|
|
242
|
+
export function createSpacer(height = 500, doc = global.document) {
|
|
243
|
+
const spacer = doc.createElement('div');
|
|
244
|
+
spacer.style.cssText = `height: ${height}px; background: #f0f0f0;`;
|
|
245
|
+
spacer.textContent = `Spacer (${height}px)`;
|
|
246
|
+
return spacer;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Add element to DOM
|
|
251
|
+
* @param {HTMLElement} element - Element to add
|
|
252
|
+
* @param {HTMLElement} parent - Parent element (default: body)
|
|
253
|
+
*/
|
|
254
|
+
export function appendElement(element, parent = global.document?.body) {
|
|
255
|
+
if (parent) {
|
|
256
|
+
parent.appendChild(element);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Remove element from DOM
|
|
262
|
+
* @param {HTMLElement} element - Element to remove
|
|
263
|
+
*/
|
|
264
|
+
export function removeElement(element) {
|
|
265
|
+
if (element?.parentElement) {
|
|
266
|
+
element.remove();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Clone element for comparison
|
|
272
|
+
* @param {HTMLElement} element - Element to clone
|
|
273
|
+
* @returns {Object} - Element state snapshot
|
|
274
|
+
*/
|
|
275
|
+
export function snapshotElement(element) {
|
|
276
|
+
return {
|
|
277
|
+
classList: Array.from(element.classList),
|
|
278
|
+
attributes: {
|
|
279
|
+
'data-animation': element.getAttribute('data-animation'),
|
|
280
|
+
'data-sentinel-id': element.getAttribute('data-sentinel-id')
|
|
281
|
+
},
|
|
282
|
+
styles: {
|
|
283
|
+
'--duration': element.style.getPropertyValue('--duration'),
|
|
284
|
+
'--delay': element.style.getPropertyValue('--delay')
|
|
285
|
+
},
|
|
286
|
+
isVisible: element.classList.contains('is-visible')
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export default {
|
|
291
|
+
createTestElement,
|
|
292
|
+
createTestElements,
|
|
293
|
+
getSentinel,
|
|
294
|
+
hasAnimation,
|
|
295
|
+
isAnimating,
|
|
296
|
+
getAnimationType,
|
|
297
|
+
getCSSVariable,
|
|
298
|
+
createMockResizeObserver,
|
|
299
|
+
setupTestDOM,
|
|
300
|
+
measurePerformance,
|
|
301
|
+
createSpacer,
|
|
302
|
+
appendElement,
|
|
303
|
+
removeElement,
|
|
304
|
+
snapshotElement
|
|
305
|
+
};
|
package/dist/animate.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { calculateRootMargin, ANIMATION_TYPES } from './animations.js';
|
|
2
|
+
import { setCSSVariables, setupAnimationElement, checkAndWarnIfCSSNotLoaded } from './dom-utils.js';
|
|
3
|
+
import { createManagedObserver, disconnectObserver } from './observer-utils.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Svelte action for scroll animations
|
|
7
|
+
* Triggers animation once when element enters viewport
|
|
8
|
+
*
|
|
9
|
+
* @param {HTMLElement} node - The element to animate
|
|
10
|
+
* @param {import('./types.js').AnimateOptions} [options={}] - Animation configuration
|
|
11
|
+
* @returns {{ update: (newOptions: import('./types.js').AnimateOptions) => void, destroy: () => void }}
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```svelte
|
|
15
|
+
* <div use:animate={{ animation: 'fade-up', duration: 1000 }}>
|
|
16
|
+
* Content
|
|
17
|
+
* </div>
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export const animate = (node, options = {}) => {
|
|
21
|
+
// SSR Guard: Return no-op action when running on server
|
|
22
|
+
if (typeof window === 'undefined') {
|
|
23
|
+
return {
|
|
24
|
+
update: () => {},
|
|
25
|
+
destroy: () => {}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Warn if CSS is not loaded (first time only)
|
|
30
|
+
if (typeof document !== 'undefined') {
|
|
31
|
+
checkAndWarnIfCSSNotLoaded();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let {
|
|
35
|
+
animation = 'fade-in',
|
|
36
|
+
duration = 2500,
|
|
37
|
+
delay = 0,
|
|
38
|
+
offset,
|
|
39
|
+
threshold = 0,
|
|
40
|
+
rootMargin,
|
|
41
|
+
onVisible
|
|
42
|
+
} = options;
|
|
43
|
+
|
|
44
|
+
// Validate animation type
|
|
45
|
+
if (animation && !ANIMATION_TYPES.includes(animation)) {
|
|
46
|
+
console.warn(
|
|
47
|
+
`[rune-scroller] Invalid animation "${animation}". Using "fade-in" instead. ` +
|
|
48
|
+
`Valid options: ${ANIMATION_TYPES.join(', ')}`
|
|
49
|
+
);
|
|
50
|
+
animation = 'fade-in';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Calculate rootMargin from offset (0-100%)
|
|
54
|
+
let finalRootMargin = calculateRootMargin(offset, rootMargin);
|
|
55
|
+
|
|
56
|
+
// Setup animation with utilities
|
|
57
|
+
setupAnimationElement(node, animation);
|
|
58
|
+
setCSSVariables(node, duration, delay);
|
|
59
|
+
|
|
60
|
+
// Track if animation has been triggered
|
|
61
|
+
let animated = false;
|
|
62
|
+
const state = { isConnected: true };
|
|
63
|
+
|
|
64
|
+
// Create IntersectionObserver for one-time animation
|
|
65
|
+
const { observer } = createManagedObserver(
|
|
66
|
+
node,
|
|
67
|
+
(entries) => {
|
|
68
|
+
entries.forEach((entry) => {
|
|
69
|
+
// Trigger animation once when element enters viewport
|
|
70
|
+
if (entry.isIntersecting && !animated) {
|
|
71
|
+
node.classList.add('is-visible');
|
|
72
|
+
// Call onVisible callback if provided
|
|
73
|
+
onVisible?.(node);
|
|
74
|
+
animated = true;
|
|
75
|
+
// Stop observing after animation triggers
|
|
76
|
+
disconnectObserver(observer, state);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
threshold,
|
|
82
|
+
rootMargin: finalRootMargin
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
update(newOptions) {
|
|
88
|
+
const {
|
|
89
|
+
duration: newDuration,
|
|
90
|
+
delay: newDelay,
|
|
91
|
+
animation: newAnimation,
|
|
92
|
+
offset: newOffset,
|
|
93
|
+
threshold: newThreshold,
|
|
94
|
+
rootMargin: newRootMargin
|
|
95
|
+
} = newOptions;
|
|
96
|
+
|
|
97
|
+
// Update CSS properties
|
|
98
|
+
if (newDuration !== undefined) {
|
|
99
|
+
duration = newDuration;
|
|
100
|
+
setCSSVariables(node, duration, newDelay ?? delay);
|
|
101
|
+
}
|
|
102
|
+
if (newDelay !== undefined && newDelay !== delay) {
|
|
103
|
+
delay = newDelay;
|
|
104
|
+
setCSSVariables(node, duration, delay);
|
|
105
|
+
}
|
|
106
|
+
if (newAnimation && newAnimation !== animation) {
|
|
107
|
+
// Validate animation type
|
|
108
|
+
if (!ANIMATION_TYPES.includes(newAnimation)) {
|
|
109
|
+
console.warn(
|
|
110
|
+
`[rune-scroller] Invalid animation "${newAnimation}". Keeping "${animation}". ` +
|
|
111
|
+
`Valid options: ${ANIMATION_TYPES.join(', ')}`
|
|
112
|
+
);
|
|
113
|
+
} else {
|
|
114
|
+
animation = newAnimation;
|
|
115
|
+
node.setAttribute('data-animation', newAnimation);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Recreate observer if threshold or rootMargin changed
|
|
120
|
+
if (newThreshold !== undefined || newOffset !== undefined || newRootMargin !== undefined) {
|
|
121
|
+
disconnectObserver(observer, state);
|
|
122
|
+
threshold = newThreshold ?? threshold;
|
|
123
|
+
offset = newOffset ?? offset;
|
|
124
|
+
rootMargin = newRootMargin ?? rootMargin;
|
|
125
|
+
finalRootMargin = calculateRootMargin(offset, rootMargin);
|
|
126
|
+
|
|
127
|
+
if (!animated) {
|
|
128
|
+
const newObserver = new IntersectionObserver(
|
|
129
|
+
(entries) => {
|
|
130
|
+
entries.forEach((entry) => {
|
|
131
|
+
if (entry.isIntersecting && !animated) {
|
|
132
|
+
node.classList.add('is-visible');
|
|
133
|
+
// Call onVisible callback if provided
|
|
134
|
+
onVisible?.(node);
|
|
135
|
+
animated = true;
|
|
136
|
+
disconnectObserver(newObserver, state);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
},
|
|
140
|
+
{ threshold, rootMargin: finalRootMargin }
|
|
141
|
+
);
|
|
142
|
+
newObserver.observe(node);
|
|
143
|
+
state.isConnected = true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
destroy() {
|
|
149
|
+
disconnectObserver(observer, state);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
};
|