smippo 0.1.0 → 0.1.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/package.json +1 -1
- package/src/cli.js +64 -1
- package/src/crawler.js +7 -0
- package/src/delete.js +188 -0
- package/src/page-capture.js +383 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "smippo",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "S.M.I.P.P.O. — Structured Mirroring of Internet Pages and Public Objects. Modern website copier that captures sites exactly as they appear in your browser.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
package/src/cli.js
CHANGED
|
@@ -89,8 +89,52 @@ export function run() {
|
|
|
89
89
|
'Wait strategy: networkidle|load|domcontentloaded',
|
|
90
90
|
'networkidle',
|
|
91
91
|
)
|
|
92
|
-
.option(
|
|
92
|
+
.option(
|
|
93
|
+
'--wait-time <ms>',
|
|
94
|
+
'Additional wait time after network idle',
|
|
95
|
+
'500',
|
|
96
|
+
)
|
|
93
97
|
.option('--timeout <ms>', 'Page load timeout', '30000')
|
|
98
|
+
|
|
99
|
+
// Scroll and reveal options (for capturing dynamic content)
|
|
100
|
+
.option(
|
|
101
|
+
'--scroll',
|
|
102
|
+
'Pre-scroll page to trigger lazy content (default: true)',
|
|
103
|
+
)
|
|
104
|
+
.option('--no-scroll', 'Disable pre-scroll behavior')
|
|
105
|
+
.option(
|
|
106
|
+
'--scroll-wait <ms>',
|
|
107
|
+
'Wait time after scrolling for animations',
|
|
108
|
+
'1000',
|
|
109
|
+
)
|
|
110
|
+
.option(
|
|
111
|
+
'--scroll-step <px>',
|
|
112
|
+
'Pixels per scroll increment (default: 200)',
|
|
113
|
+
'200',
|
|
114
|
+
)
|
|
115
|
+
.option(
|
|
116
|
+
'--scroll-delay <ms>',
|
|
117
|
+
'Delay between scroll steps (default: 50)',
|
|
118
|
+
'50',
|
|
119
|
+
)
|
|
120
|
+
.option(
|
|
121
|
+
'--scroll-behavior <type>',
|
|
122
|
+
'Scroll behavior: smooth|instant (default: smooth)',
|
|
123
|
+
'smooth',
|
|
124
|
+
)
|
|
125
|
+
.option(
|
|
126
|
+
'--reveal-all',
|
|
127
|
+
'Force reveal scroll-triggered content like GSAP, AOS (default: true)',
|
|
128
|
+
)
|
|
129
|
+
.option(
|
|
130
|
+
'--no-reveal-all',
|
|
131
|
+
'Disable force-reveal of scroll-triggered content',
|
|
132
|
+
)
|
|
133
|
+
.option(
|
|
134
|
+
'--reduced-motion',
|
|
135
|
+
'Use prefers-reduced-motion for accessibility (default: true)',
|
|
136
|
+
)
|
|
137
|
+
.option('--no-reduced-motion', 'Disable reduced motion preference')
|
|
94
138
|
.option('--user-agent <string>', 'Custom user agent')
|
|
95
139
|
.option('--viewport <WxH>', 'Viewport size', '1920x1080')
|
|
96
140
|
.option('--device <name>', 'Emulate device (e.g., "iPhone 13")')
|
|
@@ -217,6 +261,18 @@ export function run() {
|
|
|
217
261
|
});
|
|
218
262
|
});
|
|
219
263
|
|
|
264
|
+
// Delete command - remove captured sites
|
|
265
|
+
program
|
|
266
|
+
.command('delete')
|
|
267
|
+
.alias('rm')
|
|
268
|
+
.description('Delete captured sites from local storage')
|
|
269
|
+
.option('-a, --all', 'Delete all captured sites')
|
|
270
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
271
|
+
.action(async options => {
|
|
272
|
+
const {deleteSites} = await import('./delete.js');
|
|
273
|
+
await deleteSites(options);
|
|
274
|
+
});
|
|
275
|
+
|
|
220
276
|
// Screenshot capture command
|
|
221
277
|
program
|
|
222
278
|
.command('capture <url>')
|
|
@@ -312,6 +368,13 @@ async function capture(url, options) {
|
|
|
312
368
|
wait: options.wait,
|
|
313
369
|
waitTime: parseInt(options.waitTime, 10),
|
|
314
370
|
timeout: parseInt(options.timeout, 10),
|
|
371
|
+
scroll: options.scroll,
|
|
372
|
+
scrollWait: parseInt(options.scrollWait, 10),
|
|
373
|
+
scrollStep: parseInt(options.scrollStep, 10),
|
|
374
|
+
scrollDelay: parseInt(options.scrollDelay, 10),
|
|
375
|
+
scrollBehavior: options.scrollBehavior,
|
|
376
|
+
revealAll: options.revealAll,
|
|
377
|
+
reducedMotion: options.reducedMotion,
|
|
315
378
|
userAgent: options.userAgent,
|
|
316
379
|
viewport: parseViewport(options.viewport),
|
|
317
380
|
device: options.device,
|
package/src/crawler.js
CHANGED
|
@@ -270,6 +270,13 @@ export class Crawler extends EventEmitter {
|
|
|
270
270
|
mimeExclude: this.options.mimeExclude,
|
|
271
271
|
maxSize: this.options.maxSize,
|
|
272
272
|
minSize: this.options.minSize,
|
|
273
|
+
scroll: this.options.scroll,
|
|
274
|
+
scrollWait: this.options.scrollWait,
|
|
275
|
+
scrollStep: this.options.scrollStep,
|
|
276
|
+
scrollDelay: this.options.scrollDelay,
|
|
277
|
+
scrollBehavior: this.options.scrollBehavior,
|
|
278
|
+
revealAll: this.options.revealAll,
|
|
279
|
+
reducedMotion: this.options.reducedMotion,
|
|
273
280
|
});
|
|
274
281
|
|
|
275
282
|
const result = await capture.capture(url);
|
package/src/delete.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import * as p from '@clack/prompts';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import {readGlobalManifest, writeGlobalManifest} from './utils/home.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Format file size for display
|
|
10
|
+
*/
|
|
11
|
+
function formatSize(bytes) {
|
|
12
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
13
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
14
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Calculate directory size recursively
|
|
19
|
+
*/
|
|
20
|
+
async function getDirectorySize(dirPath) {
|
|
21
|
+
let totalSize = 0;
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const entries = await fs.readdir(dirPath, {withFileTypes: true});
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
27
|
+
if (entry.isDirectory()) {
|
|
28
|
+
totalSize += await getDirectorySize(fullPath);
|
|
29
|
+
} else {
|
|
30
|
+
const stats = await fs.stat(fullPath);
|
|
31
|
+
totalSize += stats.size;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
} catch (e) {
|
|
35
|
+
// Ignore errors
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return totalSize;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Interactive delete command for captured sites
|
|
43
|
+
*/
|
|
44
|
+
export async function deleteSites(options = {}) {
|
|
45
|
+
// Read global manifest
|
|
46
|
+
const manifest = await readGlobalManifest();
|
|
47
|
+
|
|
48
|
+
if (!manifest.sites || manifest.sites.length === 0) {
|
|
49
|
+
console.log(chalk.yellow('\nNo captured sites found.\n'));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Show header
|
|
54
|
+
console.log('');
|
|
55
|
+
p.intro(chalk.bgRed.white(' Delete Captured Sites '));
|
|
56
|
+
|
|
57
|
+
// Get site info with sizes
|
|
58
|
+
const sitesWithInfo = await Promise.all(
|
|
59
|
+
manifest.sites.map(async site => {
|
|
60
|
+
const exists = await fs.pathExists(site.path);
|
|
61
|
+
let size = 0;
|
|
62
|
+
if (exists) {
|
|
63
|
+
size = await getDirectorySize(site.path);
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
...site,
|
|
67
|
+
exists,
|
|
68
|
+
size,
|
|
69
|
+
displaySize: formatSize(size),
|
|
70
|
+
};
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// Filter to only existing sites
|
|
75
|
+
const existingSites = sitesWithInfo.filter(s => s.exists);
|
|
76
|
+
|
|
77
|
+
if (existingSites.length === 0) {
|
|
78
|
+
console.log(chalk.yellow('No captured sites found on disk.\n'));
|
|
79
|
+
|
|
80
|
+
// Clean up manifest
|
|
81
|
+
manifest.sites = [];
|
|
82
|
+
await writeGlobalManifest(manifest);
|
|
83
|
+
console.log(chalk.dim('Cleaned up manifest.\n'));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let sitesToDelete = [];
|
|
88
|
+
|
|
89
|
+
if (options.all) {
|
|
90
|
+
// Delete all sites
|
|
91
|
+
sitesToDelete = existingSites;
|
|
92
|
+
} else {
|
|
93
|
+
// Interactive selection
|
|
94
|
+
const selected = await p.multiselect({
|
|
95
|
+
message: 'Select sites to delete (space to select, enter to confirm):',
|
|
96
|
+
options: existingSites.map(site => ({
|
|
97
|
+
value: site,
|
|
98
|
+
label: `${site.domain}`,
|
|
99
|
+
hint: `${site.displaySize} • ${site.path}`,
|
|
100
|
+
})),
|
|
101
|
+
required: false,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (p.isCancel(selected) || !selected || selected.length === 0) {
|
|
105
|
+
p.cancel('No sites selected.');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
sitesToDelete = selected;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Show what will be deleted
|
|
113
|
+
console.log('');
|
|
114
|
+
console.log(chalk.bold('Sites to delete:'));
|
|
115
|
+
for (const site of sitesToDelete) {
|
|
116
|
+
console.log(chalk.red(` • ${site.domain} (${site.displaySize})`));
|
|
117
|
+
console.log(chalk.dim(` ${site.path}`));
|
|
118
|
+
}
|
|
119
|
+
console.log('');
|
|
120
|
+
|
|
121
|
+
// Calculate total size
|
|
122
|
+
const totalSize = sitesToDelete.reduce((sum, s) => sum + s.size, 0);
|
|
123
|
+
console.log(
|
|
124
|
+
chalk.bold(
|
|
125
|
+
`Total: ${sitesToDelete.length} site(s), ${formatSize(totalSize)}`,
|
|
126
|
+
),
|
|
127
|
+
);
|
|
128
|
+
console.log('');
|
|
129
|
+
|
|
130
|
+
// Confirmation
|
|
131
|
+
let confirmed = options.yes;
|
|
132
|
+
if (!confirmed) {
|
|
133
|
+
confirmed = await p.confirm({
|
|
134
|
+
message: 'Are you sure you want to delete these sites?',
|
|
135
|
+
initialValue: false,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (p.isCancel(confirmed) || !confirmed) {
|
|
140
|
+
p.cancel('Deletion cancelled.');
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Delete the sites
|
|
145
|
+
const spinner = p.spinner();
|
|
146
|
+
spinner.start('Deleting sites...');
|
|
147
|
+
|
|
148
|
+
let deletedCount = 0;
|
|
149
|
+
let failedCount = 0;
|
|
150
|
+
const deletedDomains = new Set();
|
|
151
|
+
|
|
152
|
+
for (const site of sitesToDelete) {
|
|
153
|
+
try {
|
|
154
|
+
await fs.remove(site.path);
|
|
155
|
+
deletedCount++;
|
|
156
|
+
deletedDomains.add(site.domain);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
failedCount++;
|
|
159
|
+
console.error(
|
|
160
|
+
chalk.red(`\n Failed to delete ${site.domain}: ${error.message}`),
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Update manifest - remove deleted sites
|
|
166
|
+
manifest.sites = manifest.sites.filter(
|
|
167
|
+
s =>
|
|
168
|
+
!deletedDomains.has(s.domain) ||
|
|
169
|
+
!sitesToDelete.some(d => d.path === s.path),
|
|
170
|
+
);
|
|
171
|
+
await writeGlobalManifest(manifest);
|
|
172
|
+
|
|
173
|
+
spinner.stop('Deletion complete!');
|
|
174
|
+
|
|
175
|
+
// Summary
|
|
176
|
+
console.log('');
|
|
177
|
+
if (deletedCount > 0) {
|
|
178
|
+
console.log(
|
|
179
|
+
chalk.green(
|
|
180
|
+
`✓ Deleted ${deletedCount} site(s), freed ${formatSize(totalSize)}`,
|
|
181
|
+
),
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
if (failedCount > 0) {
|
|
185
|
+
console.log(chalk.red(`✗ Failed to delete ${failedCount} site(s)`));
|
|
186
|
+
}
|
|
187
|
+
console.log('');
|
|
188
|
+
}
|
package/src/page-capture.js
CHANGED
|
@@ -3,6 +3,8 @@ import {extractLinks} from './link-extractor.js';
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Capture a single page with all its rendered content
|
|
6
|
+
* Uses best-in-class techniques including accessibility features,
|
|
7
|
+
* reduced motion, and human-like scrolling behavior
|
|
6
8
|
*/
|
|
7
9
|
export class PageCapture {
|
|
8
10
|
constructor(page, options = {}) {
|
|
@@ -22,6 +24,12 @@ export class PageCapture {
|
|
|
22
24
|
await this._collectResource(response);
|
|
23
25
|
});
|
|
24
26
|
|
|
27
|
+
// Step 0: Enable reduced motion to disable CSS animations (accessibility feature)
|
|
28
|
+
// This causes many sites to show final animation states immediately
|
|
29
|
+
if (this.options.reducedMotion !== false) {
|
|
30
|
+
await this.page.emulateMedia({reducedMotion: 'reduce'});
|
|
31
|
+
}
|
|
32
|
+
|
|
25
33
|
// Navigate to the page
|
|
26
34
|
try {
|
|
27
35
|
await this.page.goto(url, {
|
|
@@ -35,11 +43,40 @@ export class PageCapture {
|
|
|
35
43
|
}
|
|
36
44
|
}
|
|
37
45
|
|
|
38
|
-
// Additional wait time if specified
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
// Additional wait time if specified (default 500ms for animations to start)
|
|
47
|
+
const waitTime = this.options.waitTime ?? 500;
|
|
48
|
+
if (waitTime > 0) {
|
|
49
|
+
await this.page.waitForTimeout(waitTime);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Step 1: Force all scroll-triggered animations to their END state (100% progress)
|
|
53
|
+
// This is different from killing them - we want their final visual state
|
|
54
|
+
if (this.options.revealAll !== false) {
|
|
55
|
+
await this._completeAllAnimations();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Step 2: Human-like scrolling to trigger lazy content and intersection observers
|
|
59
|
+
if (this.options.scroll !== false) {
|
|
60
|
+
await this._humanLikeScroll();
|
|
41
61
|
}
|
|
42
62
|
|
|
63
|
+
// Step 3: Complete animations again after scroll (new elements may have appeared)
|
|
64
|
+
if (this.options.revealAll !== false) {
|
|
65
|
+
await this._completeAllAnimations();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Step 4: Final wait for any remaining animations/network requests
|
|
69
|
+
const scrollWait = this.options.scrollWait ?? 1000;
|
|
70
|
+
if (scrollWait > 0) {
|
|
71
|
+
await this.page.waitForTimeout(scrollWait);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Step 5: Wait for network to be truly idle (no pending requests)
|
|
75
|
+
await this._waitForNetworkIdle();
|
|
76
|
+
|
|
77
|
+
// Step 6: Force reveal any remaining hidden elements
|
|
78
|
+
await this._forceRevealHiddenElements();
|
|
79
|
+
|
|
43
80
|
// Get the rendered HTML
|
|
44
81
|
const html = await this.page.content();
|
|
45
82
|
|
|
@@ -148,4 +185,347 @@ export class PageCapture {
|
|
|
148
185
|
return type === filter || type.startsWith(filter + ';');
|
|
149
186
|
});
|
|
150
187
|
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Wait for network to be truly idle (no pending requests for a period)
|
|
191
|
+
*/
|
|
192
|
+
async _waitForNetworkIdle() {
|
|
193
|
+
try {
|
|
194
|
+
await this.page.waitForLoadState('networkidle', {timeout: 5000});
|
|
195
|
+
} catch {
|
|
196
|
+
// Timeout is ok, continue anyway
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Human-like scrolling behavior with pauses and mouse movements
|
|
202
|
+
* Triggers intersection observers and lazy loading more naturally
|
|
203
|
+
*/
|
|
204
|
+
async _humanLikeScroll() {
|
|
205
|
+
const scrollStep = this.options.scrollStep || 300;
|
|
206
|
+
const scrollDelay = this.options.scrollDelay || 100;
|
|
207
|
+
|
|
208
|
+
/* eslint-disable no-undef */
|
|
209
|
+
await this.page.evaluate(
|
|
210
|
+
async ({step, delay}) => {
|
|
211
|
+
// Human-like smooth scroll with easing
|
|
212
|
+
const smoothScrollTo = (targetY, duration = 500) => {
|
|
213
|
+
return new Promise(resolve => {
|
|
214
|
+
const startY = window.scrollY;
|
|
215
|
+
const distance = targetY - startY;
|
|
216
|
+
const startTime = performance.now();
|
|
217
|
+
|
|
218
|
+
const easeInOutCubic = t =>
|
|
219
|
+
t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
|
220
|
+
|
|
221
|
+
const animate = () => {
|
|
222
|
+
const elapsed = performance.now() - startTime;
|
|
223
|
+
const progress = Math.min(elapsed / duration, 1);
|
|
224
|
+
const eased = easeInOutCubic(progress);
|
|
225
|
+
|
|
226
|
+
window.scrollTo(0, startY + distance * eased);
|
|
227
|
+
|
|
228
|
+
if (progress < 1) {
|
|
229
|
+
requestAnimationFrame(animate);
|
|
230
|
+
} else {
|
|
231
|
+
resolve();
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
requestAnimationFrame(animate);
|
|
235
|
+
});
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// Wait helper
|
|
239
|
+
const wait = ms => new Promise(r => setTimeout(r, ms));
|
|
240
|
+
|
|
241
|
+
// Get viewport and page dimensions
|
|
242
|
+
const viewportHeight = window.innerHeight;
|
|
243
|
+
let pageHeight = document.body.scrollHeight;
|
|
244
|
+
let currentY = 0;
|
|
245
|
+
let lastHeight = pageHeight;
|
|
246
|
+
|
|
247
|
+
// Phase 1: Scroll down slowly, pausing at each "section"
|
|
248
|
+
while (currentY < pageHeight) {
|
|
249
|
+
// Scroll one viewport at a time (like a human reading)
|
|
250
|
+
const targetY = Math.min(currentY + step, pageHeight);
|
|
251
|
+
await smoothScrollTo(targetY, delay * 3);
|
|
252
|
+
|
|
253
|
+
// Pause longer at section boundaries (every viewport height)
|
|
254
|
+
if (
|
|
255
|
+
Math.floor(currentY / viewportHeight) !==
|
|
256
|
+
Math.floor(targetY / viewportHeight)
|
|
257
|
+
) {
|
|
258
|
+
await wait(delay * 2); // Pause to "read" the section
|
|
259
|
+
} else {
|
|
260
|
+
await wait(delay);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
currentY = targetY;
|
|
264
|
+
|
|
265
|
+
// Check for lazy-loaded content increasing page height
|
|
266
|
+
const newHeight = document.body.scrollHeight;
|
|
267
|
+
if (newHeight > lastHeight) {
|
|
268
|
+
pageHeight = newHeight;
|
|
269
|
+
lastHeight = newHeight;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Phase 2: Ensure we're at the very bottom
|
|
274
|
+
await smoothScrollTo(document.body.scrollHeight, 300);
|
|
275
|
+
await wait(500); // Wait at bottom
|
|
276
|
+
|
|
277
|
+
// Check one more time for lazy content
|
|
278
|
+
if (document.body.scrollHeight > pageHeight) {
|
|
279
|
+
await smoothScrollTo(document.body.scrollHeight, 300);
|
|
280
|
+
await wait(300);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Phase 3: Scroll back to top (faster)
|
|
284
|
+
await smoothScrollTo(0, 800);
|
|
285
|
+
await wait(300);
|
|
286
|
+
},
|
|
287
|
+
{step: scrollStep, delay: scrollDelay},
|
|
288
|
+
);
|
|
289
|
+
/* eslint-enable no-undef */
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Complete all animations to their final state (100% progress)
|
|
294
|
+
* Instead of killing animations, we progress them to completion
|
|
295
|
+
*/
|
|
296
|
+
async _completeAllAnimations() {
|
|
297
|
+
/* eslint-disable no-undef */
|
|
298
|
+
await this.page.evaluate(() => {
|
|
299
|
+
// Helper to safely access nested properties
|
|
300
|
+
const safeGet = (obj, path) => {
|
|
301
|
+
try {
|
|
302
|
+
return path.split('.').reduce((o, k) => o?.[k], obj);
|
|
303
|
+
} catch {
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// 1. GSAP - Progress ALL tweens and timelines to 100%
|
|
309
|
+
const gsap = safeGet(window, 'gsap');
|
|
310
|
+
if (gsap) {
|
|
311
|
+
try {
|
|
312
|
+
// Get all global tweens and progress them to end
|
|
313
|
+
const tweens =
|
|
314
|
+
gsap.globalTimeline?.getChildren?.(true, true, true) || [];
|
|
315
|
+
tweens.forEach(tween => {
|
|
316
|
+
try {
|
|
317
|
+
// Progress to 100% (end state)
|
|
318
|
+
tween.progress?.(1, true);
|
|
319
|
+
// Also try totalProgress for timelines
|
|
320
|
+
tween.totalProgress?.(1, true);
|
|
321
|
+
} catch (e) {
|
|
322
|
+
/* ignore */
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
} catch (e) {
|
|
326
|
+
/* ignore */
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// 2. GSAP ScrollTrigger - Progress all triggers to their END state
|
|
331
|
+
const ScrollTrigger =
|
|
332
|
+
safeGet(window, 'ScrollTrigger') ||
|
|
333
|
+
safeGet(window, 'gsap.ScrollTrigger');
|
|
334
|
+
if (ScrollTrigger) {
|
|
335
|
+
try {
|
|
336
|
+
const triggers = ScrollTrigger.getAll?.() || [];
|
|
337
|
+
triggers.forEach(trigger => {
|
|
338
|
+
try {
|
|
339
|
+
// Progress the trigger to 100% (fully scrolled past)
|
|
340
|
+
if (trigger.animation) {
|
|
341
|
+
trigger.animation.progress?.(1, true);
|
|
342
|
+
trigger.animation.totalProgress?.(1, true);
|
|
343
|
+
}
|
|
344
|
+
// Also set the trigger's own progress
|
|
345
|
+
trigger.progress?.(1, true);
|
|
346
|
+
|
|
347
|
+
// Disable the trigger so it doesn't reset
|
|
348
|
+
trigger.disable?.();
|
|
349
|
+
} catch (e) {
|
|
350
|
+
/* ignore */
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// Don't refresh - we want to keep the end states
|
|
355
|
+
} catch (e) {
|
|
356
|
+
/* ignore */
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// 3. Web Animations API - Complete all animations
|
|
361
|
+
document.getAnimations?.().forEach(animation => {
|
|
362
|
+
try {
|
|
363
|
+
animation.finish();
|
|
364
|
+
} catch (e) {
|
|
365
|
+
try {
|
|
366
|
+
animation.currentTime =
|
|
367
|
+
animation.effect?.getTiming?.()?.duration || 0;
|
|
368
|
+
} catch (e2) {
|
|
369
|
+
/* ignore */
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// 4. AOS - Mark all as animated
|
|
375
|
+
document.querySelectorAll('[data-aos]').forEach(el => {
|
|
376
|
+
el.classList.add('aos-animate');
|
|
377
|
+
el.setAttribute('data-aos-animate', 'true');
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// 5. WOW.js elements
|
|
381
|
+
document.querySelectorAll('.wow').forEach(el => {
|
|
382
|
+
el.classList.add('animated');
|
|
383
|
+
el.style.visibility = 'visible';
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// 6. ScrollReveal elements
|
|
387
|
+
document.querySelectorAll('[data-sr-id]').forEach(el => {
|
|
388
|
+
el.style.visibility = 'visible';
|
|
389
|
+
el.style.opacity = '1';
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// 7. Anime.js - Complete all animations
|
|
393
|
+
const anime = safeGet(window, 'anime');
|
|
394
|
+
if (anime?.running) {
|
|
395
|
+
anime.running.forEach(anim => {
|
|
396
|
+
try {
|
|
397
|
+
anim.seek?.(anim.duration);
|
|
398
|
+
} catch (e) {
|
|
399
|
+
/* ignore */
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// 8. Lottie animations - Go to last frame
|
|
405
|
+
document.querySelectorAll('lottie-player, [data-lottie]').forEach(el => {
|
|
406
|
+
try {
|
|
407
|
+
el.goToAndStop?.(el.totalFrames - 1, true);
|
|
408
|
+
el.pause?.();
|
|
409
|
+
} catch (e) {
|
|
410
|
+
/* ignore */
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
/* eslint-enable no-undef */
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Force reveal any remaining hidden elements
|
|
419
|
+
* This is the final cleanup pass
|
|
420
|
+
*/
|
|
421
|
+
async _forceRevealHiddenElements() {
|
|
422
|
+
/* eslint-disable no-undef */
|
|
423
|
+
await this.page.evaluate(() => {
|
|
424
|
+
// 1. Force load all lazy images
|
|
425
|
+
document.querySelectorAll('img').forEach(img => {
|
|
426
|
+
// Load from data attributes
|
|
427
|
+
const lazySrc =
|
|
428
|
+
img.dataset.src || img.dataset.lazy || img.dataset.lazySrc;
|
|
429
|
+
if (lazySrc && !img.src.includes(lazySrc)) {
|
|
430
|
+
img.src = lazySrc;
|
|
431
|
+
}
|
|
432
|
+
// Remove lazy loading attribute
|
|
433
|
+
img.removeAttribute('loading');
|
|
434
|
+
// Trigger load if not loaded
|
|
435
|
+
if (!img.complete) {
|
|
436
|
+
img.loading = 'eager';
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// 2. Load lazy srcset
|
|
441
|
+
document.querySelectorAll('source[data-srcset]').forEach(source => {
|
|
442
|
+
if (source.dataset.srcset) {
|
|
443
|
+
source.srcset = source.dataset.srcset;
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// 3. Load lazy background images
|
|
448
|
+
document
|
|
449
|
+
.querySelectorAll(
|
|
450
|
+
'[data-bg], [data-background], [data-background-image]',
|
|
451
|
+
)
|
|
452
|
+
.forEach(el => {
|
|
453
|
+
const bg =
|
|
454
|
+
el.dataset.bg ||
|
|
455
|
+
el.dataset.background ||
|
|
456
|
+
el.dataset.backgroundImage;
|
|
457
|
+
if (bg) {
|
|
458
|
+
el.style.backgroundImage = `url("${bg}")`;
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// 4. Load lazy iframes
|
|
463
|
+
document.querySelectorAll('iframe[data-src]').forEach(iframe => {
|
|
464
|
+
if (iframe.dataset.src) {
|
|
465
|
+
iframe.src = iframe.dataset.src;
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// 5. Fix elements with opacity: 0 (likely animation end states)
|
|
470
|
+
document.querySelectorAll('*').forEach(el => {
|
|
471
|
+
const computed = window.getComputedStyle(el);
|
|
472
|
+
|
|
473
|
+
// Only fix visible containers that have hidden content
|
|
474
|
+
if (computed.opacity === '0' && !el.getAttribute('aria-hidden')) {
|
|
475
|
+
// Check if this is likely an animated element
|
|
476
|
+
const hasAnimClass =
|
|
477
|
+
el.className && /anim|fade|slide|reveal|show/i.test(el.className);
|
|
478
|
+
const hasAnimAttr =
|
|
479
|
+
el.hasAttribute('data-aos') ||
|
|
480
|
+
el.hasAttribute('data-animate') ||
|
|
481
|
+
el.hasAttribute('data-scroll');
|
|
482
|
+
|
|
483
|
+
if (hasAnimClass || hasAnimAttr || el.style.opacity === '0') {
|
|
484
|
+
el.style.setProperty('opacity', '1', 'important');
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Fix visibility
|
|
489
|
+
if (
|
|
490
|
+
computed.visibility === 'hidden' &&
|
|
491
|
+
!el.getAttribute('aria-hidden')
|
|
492
|
+
) {
|
|
493
|
+
const hasAnimClass =
|
|
494
|
+
el.className && /anim|fade|slide|reveal|show/i.test(el.className);
|
|
495
|
+
if (hasAnimClass || el.style.visibility === 'hidden') {
|
|
496
|
+
el.style.setProperty('visibility', 'visible', 'important');
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Fix transforms that look like animation starting positions
|
|
501
|
+
if (computed.transform && computed.transform !== 'none') {
|
|
502
|
+
const transform = computed.transform;
|
|
503
|
+
// Check for translateY/translateX that might be animation starting positions
|
|
504
|
+
if (/translate[XY]\s*\(\s*[+-]?\d+/.test(transform)) {
|
|
505
|
+
const hasAnimClass =
|
|
506
|
+
el.className && /anim|fade|slide|reveal|show/i.test(el.className);
|
|
507
|
+
if (hasAnimClass) {
|
|
508
|
+
el.style.setProperty('transform', 'none', 'important');
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
// 6. Force CSS animations to end state
|
|
515
|
+
document.querySelectorAll('*').forEach(el => {
|
|
516
|
+
const computed = window.getComputedStyle(el);
|
|
517
|
+
if (computed.animationName && computed.animationName !== 'none') {
|
|
518
|
+
el.style.setProperty('animation', 'none', 'important');
|
|
519
|
+
}
|
|
520
|
+
if (
|
|
521
|
+
computed.transition &&
|
|
522
|
+
computed.transition !== 'none' &&
|
|
523
|
+
computed.transition !== 'all 0s ease 0s'
|
|
524
|
+
) {
|
|
525
|
+
el.style.setProperty('transition', 'none', 'important');
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
/* eslint-enable no-undef */
|
|
530
|
+
}
|
|
151
531
|
}
|