hocmai-feedback-widget 0.2.0 → 0.2.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/README.md +132 -97
- package/dist/capture/device-detection.d.ts +13 -0
- package/dist/capture/fonts.d.ts +4 -0
- package/dist/capture/index.d.ts +14 -0
- package/dist/capture/mathjax-fonts.d.ts +5 -0
- package/dist/capture/types.d.ts +25 -0
- package/dist/ckeditor.mjs +113 -0
- package/dist/ckeditor.mjs.map +1 -0
- package/dist/components/CkEditor.d.ts +14 -0
- package/dist/components/FeedbackWidget.d.ts +11 -0
- package/dist/context/FeedbackContext.d.ts +5 -0
- package/dist/hocmai-feedback-widget.mjs +7544 -0
- package/dist/hocmai-feedback-widget.mjs.map +1 -0
- package/dist/index.d.ts +3 -0
- package/package.json +32 -28
- package/src/App.tsx +143 -58
- package/src/capture/device-detection.ts +52 -0
- package/src/capture/fonts.ts +34 -0
- package/src/capture/index.ts +362 -0
- package/src/capture/mathjax-fonts.ts +143 -0
- package/src/capture/types.ts +34 -0
- package/src/ckeditor.d.ts +14 -0
- package/src/components/CkEditor.tsx +130 -0
- package/src/components/CropOverlay.tsx +29 -47
- package/src/components/FeedbackWidget.tsx +96 -90
- package/src/context/FeedbackContext.tsx +11 -4
- package/src/index.ts +9 -0
- package/src/main.tsx +7 -8
- package/src/utils/device.ts +14 -14
- package/dist/support-feedback-lib.es.js +0 -3867
- package/dist/support-feedback-lib.umd.js +0 -84
- package/dist/utils/screenshot/background-renderer.d.ts +0 -3
- package/dist/utils/screenshot/canvas-renderer.d.ts +0 -9
- package/dist/utils/screenshot/cleanup.d.ts +0 -3
- package/dist/utils/screenshot/device-detection.d.ts +0 -2
- package/dist/utils/screenshot/element-collector.d.ts +0 -10
- package/dist/utils/screenshot/image-renderer.d.ts +0 -21
- package/dist/utils/screenshot/performance.d.ts +0 -3
- package/dist/utils/screenshot/svg-renderer.d.ts +0 -24
- package/dist/utils/screenshot/text-renderer.d.ts +0 -23
- package/dist/utils/screenshot/types.d.ts +0 -35
- package/dist/utils/screenshot.d.ts +0 -9
- package/dist/vite.svg +0 -1
- package/src/App.css +0 -42
- package/src/assets/react.svg +0 -1
- package/src/index.css +0 -68
- package/src/utils/screenshot/background-renderer.ts +0 -54
- package/src/utils/screenshot/canvas-renderer.ts +0 -217
- package/src/utils/screenshot/cleanup.ts +0 -32
- package/src/utils/screenshot/device-detection.ts +0 -38
- package/src/utils/screenshot/element-collector.ts +0 -83
- package/src/utils/screenshot/image-renderer.ts +0 -148
- package/src/utils/screenshot/performance.ts +0 -57
- package/src/utils/screenshot/svg-renderer.ts +0 -126
- package/src/utils/screenshot/text-renderer.ts +0 -212
- package/src/utils/screenshot/types.ts +0 -40
- package/src/utils/screenshot.ts +0 -68
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
export function getComputedBackgroundColor(element: HTMLElement): string | null {
|
|
2
|
-
const styles = window.getComputedStyle(element);
|
|
3
|
-
const bgColor = styles.backgroundColor;
|
|
4
|
-
|
|
5
|
-
// Check if transparent
|
|
6
|
-
if (bgColor === 'transparent' || bgColor === 'rgba(0, 0, 0, 0)') {
|
|
7
|
-
return null;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
return bgColor;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export async function renderBackground(
|
|
14
|
-
element: HTMLElement,
|
|
15
|
-
ctx: CanvasRenderingContext2D
|
|
16
|
-
): Promise<void> {
|
|
17
|
-
const rect = element.getBoundingClientRect();
|
|
18
|
-
const styles = window.getComputedStyle(element);
|
|
19
|
-
|
|
20
|
-
// Use viewport coordinates directly from getBoundingClientRect
|
|
21
|
-
const x = rect.left;
|
|
22
|
-
const y = rect.top;
|
|
23
|
-
const width = rect.width;
|
|
24
|
-
const height = rect.height;
|
|
25
|
-
|
|
26
|
-
// Render background color
|
|
27
|
-
const bgColor = styles.backgroundColor;
|
|
28
|
-
if (bgColor && bgColor !== 'transparent' && bgColor !== 'rgba(0, 0, 0, 0)') {
|
|
29
|
-
ctx.fillStyle = bgColor;
|
|
30
|
-
ctx.fillRect(x, y, width, height);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Render border
|
|
34
|
-
const borderWidth = parseFloat(styles.borderWidth) || 0;
|
|
35
|
-
if (borderWidth > 0) {
|
|
36
|
-
const borderColor = styles.borderColor;
|
|
37
|
-
if (borderColor && borderColor !== 'transparent') {
|
|
38
|
-
ctx.strokeStyle = borderColor;
|
|
39
|
-
ctx.lineWidth = borderWidth;
|
|
40
|
-
ctx.strokeRect(x, y, width, height);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export async function renderBackgrounds(
|
|
46
|
-
elements: Element[],
|
|
47
|
-
ctx: CanvasRenderingContext2D
|
|
48
|
-
): Promise<void> {
|
|
49
|
-
for (const element of elements) {
|
|
50
|
-
if (element instanceof HTMLElement) {
|
|
51
|
-
await renderBackground(element, ctx);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
import type { Rect, CaptureOptions, CaptureProgress, CaptureBounds } from './types';
|
|
2
|
-
import { getDeviceCapabilities } from './device-detection';
|
|
3
|
-
import { createCleanupTracker, cleanup } from './cleanup';
|
|
4
|
-
import {
|
|
5
|
-
calculateCaptureBounds,
|
|
6
|
-
collectVisibleElements,
|
|
7
|
-
estimateDOMComplexity
|
|
8
|
-
} from './element-collector';
|
|
9
|
-
import { yieldToMainThread } from './performance';
|
|
10
|
-
import { renderBackgrounds } from './background-renderer';
|
|
11
|
-
import { getImageElements, renderImages } from './image-renderer';
|
|
12
|
-
import { getSVGElements, renderSVGs, getMathJaxElements } from './svg-renderer';
|
|
13
|
-
import { renderTextContent } from './text-renderer';
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Main canvas-based capture with layered rendering
|
|
17
|
-
*/
|
|
18
|
-
export async function captureWithCanvas(
|
|
19
|
-
cropRect: Rect | undefined,
|
|
20
|
-
options: Partial<CaptureOptions>,
|
|
21
|
-
onProgress?: (progress: CaptureProgress) => void
|
|
22
|
-
): Promise<string | null> {
|
|
23
|
-
const cleanupTracker = createCleanupTracker();
|
|
24
|
-
|
|
25
|
-
try {
|
|
26
|
-
// 1. Get device capabilities and adjust options
|
|
27
|
-
const capabilities = getDeviceCapabilities();
|
|
28
|
-
const quality = options.quality ?? capabilities.recommendedQuality;
|
|
29
|
-
const scale = options.scale ?? capabilities.recommendedScale;
|
|
30
|
-
const includeMathJax = options.includeMathJax ?? true;
|
|
31
|
-
|
|
32
|
-
onProgress?.({
|
|
33
|
-
phase: 'analyzing',
|
|
34
|
-
progress: 10,
|
|
35
|
-
message: 'Analyzing DOM structure...'
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
// 2. Analyze DOM complexity
|
|
39
|
-
const element = document.documentElement;
|
|
40
|
-
const complexity = estimateDOMComplexity(element);
|
|
41
|
-
console.log('[Canvas] DOM complexity:', complexity);
|
|
42
|
-
|
|
43
|
-
// Adjust quality based on complexity
|
|
44
|
-
let finalQuality = quality;
|
|
45
|
-
if (complexity.isComplex) {
|
|
46
|
-
finalQuality = Math.max(0.6, quality - 0.1);
|
|
47
|
-
console.log('[Canvas] Complex DOM detected, reducing quality to', finalQuality);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// 3. Calculate capture dimensions
|
|
51
|
-
const targetX = cropRect ? cropRect.x : 0;
|
|
52
|
-
const targetY = cropRect ? cropRect.y : 0;
|
|
53
|
-
const targetWidth = cropRect ? cropRect.width : window.innerWidth;
|
|
54
|
-
const targetHeight = cropRect ? cropRect.height : window.innerHeight;
|
|
55
|
-
|
|
56
|
-
// 4. Check canvas size limits
|
|
57
|
-
let finalScale = scale;
|
|
58
|
-
if (
|
|
59
|
-
targetWidth * finalScale > capabilities.maxCanvasSize ||
|
|
60
|
-
targetHeight * finalScale > capabilities.maxCanvasSize
|
|
61
|
-
) {
|
|
62
|
-
console.warn('[Canvas] Size exceeds device limits, reducing scale');
|
|
63
|
-
finalScale = Math.min(
|
|
64
|
-
capabilities.maxCanvasSize / targetWidth,
|
|
65
|
-
capabilities.maxCanvasSize / targetHeight,
|
|
66
|
-
finalScale
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// 5. Create canvas
|
|
71
|
-
const canvas = document.createElement('canvas');
|
|
72
|
-
canvas.width = targetWidth * finalScale;
|
|
73
|
-
canvas.height = targetHeight * finalScale;
|
|
74
|
-
cleanupTracker.tempCanvases.push(canvas);
|
|
75
|
-
|
|
76
|
-
const ctx = canvas.getContext('2d', {
|
|
77
|
-
alpha: false,
|
|
78
|
-
willReadFrequently: false
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
if (!ctx) {
|
|
82
|
-
throw new Error('Failed to get canvas context');
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Apply scale
|
|
86
|
-
ctx.scale(finalScale, finalScale);
|
|
87
|
-
|
|
88
|
-
// Fill with white background
|
|
89
|
-
ctx.fillStyle = '#ffffff';
|
|
90
|
-
ctx.fillRect(0, 0, targetWidth, targetHeight);
|
|
91
|
-
|
|
92
|
-
onProgress?.({
|
|
93
|
-
phase: 'analyzing',
|
|
94
|
-
progress: 20,
|
|
95
|
-
message: 'Collecting visible elements...'
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
// 6. Calculate capture bounds and collect elements
|
|
99
|
-
const bounds: CaptureBounds = calculateCaptureBounds(
|
|
100
|
-
cropRect,
|
|
101
|
-
capabilities.maxCanvasSize > 8192 ? 500 : 300 // Buffer based on device
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
const visibleElements = collectVisibleElements(bounds);
|
|
105
|
-
console.log('[Canvas] Visible elements:', visibleElements.length);
|
|
106
|
-
|
|
107
|
-
// 7. Separate elements by type
|
|
108
|
-
const images = getImageElements(visibleElements);
|
|
109
|
-
const svgs = getSVGElements(visibleElements);
|
|
110
|
-
const mathJaxElements = includeMathJax ? getMathJaxElements(visibleElements) : [];
|
|
111
|
-
|
|
112
|
-
console.log('[Canvas] Images:', images.length, 'SVGs:', svgs.length, 'MathJax:', mathJaxElements.length);
|
|
113
|
-
|
|
114
|
-
onProgress?.({
|
|
115
|
-
phase: 'rendering',
|
|
116
|
-
progress: 30,
|
|
117
|
-
message: 'Rendering backgrounds...'
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
// 8. Apply translation for crop offset
|
|
121
|
-
// When cropRect is provided, translate canvas to show only the cropped area
|
|
122
|
-
ctx.translate(-targetX, -targetY);
|
|
123
|
-
|
|
124
|
-
// 9. Layer 1: Render backgrounds
|
|
125
|
-
await renderBackgrounds(visibleElements, ctx);
|
|
126
|
-
|
|
127
|
-
await yieldToMainThread();
|
|
128
|
-
|
|
129
|
-
onProgress?.({
|
|
130
|
-
phase: 'rendering',
|
|
131
|
-
progress: 50,
|
|
132
|
-
message: `Rendering ${images.length} images...`
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
// 10. Layer 2: Render images
|
|
136
|
-
await renderImages(images, ctx, (current, total) => {
|
|
137
|
-
const progress = 50 + Math.round((current / total) * 30);
|
|
138
|
-
onProgress?.({
|
|
139
|
-
phase: 'rendering',
|
|
140
|
-
progress,
|
|
141
|
-
message: `Rendering images (${current}/${total})...`
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
await yieldToMainThread();
|
|
146
|
-
|
|
147
|
-
onProgress?.({
|
|
148
|
-
phase: 'rendering',
|
|
149
|
-
progress: 80,
|
|
150
|
-
message: `Rendering ${svgs.length} SVG elements...`
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
// 11. Layer 3: Render SVG elements (including MathJax)
|
|
154
|
-
await renderSVGs(svgs, ctx, (current, total) => {
|
|
155
|
-
const progress = 80 + Math.round((current / total) * 10);
|
|
156
|
-
onProgress?.({
|
|
157
|
-
phase: 'rendering',
|
|
158
|
-
progress,
|
|
159
|
-
message: `Rendering SVG (${current}/${total})...`
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
await yieldToMainThread();
|
|
164
|
-
|
|
165
|
-
onProgress?.({
|
|
166
|
-
phase: 'rendering',
|
|
167
|
-
progress: 90,
|
|
168
|
-
message: 'Rendering text content...'
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
// 12. Layer 4: Render text content
|
|
172
|
-
await renderTextContent(visibleElements, ctx, (current, total) => {
|
|
173
|
-
const progress = 90 + Math.round((current / total) * 5);
|
|
174
|
-
onProgress?.({
|
|
175
|
-
phase: 'rendering',
|
|
176
|
-
progress,
|
|
177
|
-
message: `Rendering text (${current}/${total})...`
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
await yieldToMainThread();
|
|
182
|
-
|
|
183
|
-
onProgress?.({
|
|
184
|
-
phase: 'exporting',
|
|
185
|
-
progress: 95,
|
|
186
|
-
message: 'Exporting canvas to image...'
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
// 13. Export canvas to data URL
|
|
190
|
-
const dataUrl = canvas.toDataURL('image/png', finalQuality);
|
|
191
|
-
|
|
192
|
-
onProgress?.({
|
|
193
|
-
phase: 'exporting',
|
|
194
|
-
progress: 100,
|
|
195
|
-
message: 'Capture complete'
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
console.log('[Canvas] Capture successful');
|
|
199
|
-
return dataUrl;
|
|
200
|
-
|
|
201
|
-
} catch (error) {
|
|
202
|
-
console.error('[Canvas] Capture failed:', error);
|
|
203
|
-
return null;
|
|
204
|
-
} finally {
|
|
205
|
-
// 14. Cleanup resources
|
|
206
|
-
cleanup(cleanupTracker);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Basic canvas capture without progress reporting
|
|
212
|
-
*/
|
|
213
|
-
export async function basicCanvasCapture(
|
|
214
|
-
cropRect?: Rect
|
|
215
|
-
): Promise<string | null> {
|
|
216
|
-
return captureWithCanvas(cropRect, {}, undefined);
|
|
217
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import type { CleanupTracker } from './types';
|
|
2
|
-
|
|
3
|
-
export function createCleanupTracker(): CleanupTracker {
|
|
4
|
-
return {
|
|
5
|
-
objectUrls: [],
|
|
6
|
-
tempCanvases: []
|
|
7
|
-
};
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function cleanup(tracker: CleanupTracker): void {
|
|
11
|
-
// Revoke all object URLs
|
|
12
|
-
tracker.objectUrls.forEach(url => {
|
|
13
|
-
try {
|
|
14
|
-
URL.revokeObjectURL(url);
|
|
15
|
-
} catch {
|
|
16
|
-
console.warn('[Screenshot] Failed to revoke URL:', url);
|
|
17
|
-
}
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
// Clear temp canvases
|
|
21
|
-
tracker.tempCanvases.forEach(canvas => {
|
|
22
|
-
const ctx = canvas.getContext('2d');
|
|
23
|
-
if (ctx) {
|
|
24
|
-
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
25
|
-
}
|
|
26
|
-
canvas.width = 0;
|
|
27
|
-
canvas.height = 0;
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
tracker.objectUrls.length = 0;
|
|
31
|
-
tracker.tempCanvases.length = 0;
|
|
32
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import type { DeviceCapabilities } from './types';
|
|
2
|
-
|
|
3
|
-
export function getDeviceCapabilities(): DeviceCapabilities {
|
|
4
|
-
const userAgent = navigator.userAgent.toLowerCase();
|
|
5
|
-
const isMobile = /iphone|ipod|android.*mobile/.test(userAgent);
|
|
6
|
-
const isTablet = /ipad|android(?!.*mobile)/.test(userAgent);
|
|
7
|
-
const deviceMemory = (navigator as { deviceMemory?: number }).deviceMemory || 4;
|
|
8
|
-
const isLowMemory = deviceMemory < 4;
|
|
9
|
-
|
|
10
|
-
if (isMobile) {
|
|
11
|
-
return {
|
|
12
|
-
maxCanvasSize: 4096,
|
|
13
|
-
recommendedScale: isLowMemory ? 0.8 : 1,
|
|
14
|
-
recommendedQuality: isLowMemory ? 0.7 : 0.8,
|
|
15
|
-
chunkSize: 30,
|
|
16
|
-
yieldInterval: 16, // ~60fps
|
|
17
|
-
maxTimeout: 5000
|
|
18
|
-
};
|
|
19
|
-
} else if (isTablet) {
|
|
20
|
-
return {
|
|
21
|
-
maxCanvasSize: 8192,
|
|
22
|
-
recommendedScale: 1,
|
|
23
|
-
recommendedQuality: 0.85,
|
|
24
|
-
chunkSize: 50,
|
|
25
|
-
yieldInterval: 16,
|
|
26
|
-
maxTimeout: 4000
|
|
27
|
-
};
|
|
28
|
-
} else {
|
|
29
|
-
return {
|
|
30
|
-
maxCanvasSize: 16384,
|
|
31
|
-
recommendedScale: 1,
|
|
32
|
-
recommendedQuality: 0.9,
|
|
33
|
-
chunkSize: 100,
|
|
34
|
-
yieldInterval: 8,
|
|
35
|
-
maxTimeout: 3000
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
}
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import type { Rect, CaptureBounds } from './types';
|
|
2
|
-
|
|
3
|
-
export function calculateCaptureBounds(
|
|
4
|
-
cropRect: Rect | undefined,
|
|
5
|
-
buffer: number
|
|
6
|
-
): CaptureBounds {
|
|
7
|
-
const targetX = cropRect ? cropRect.x : 0;
|
|
8
|
-
const targetY = cropRect ? cropRect.y : 0;
|
|
9
|
-
const targetWidth = cropRect ? cropRect.width : window.innerWidth;
|
|
10
|
-
const targetHeight = cropRect ? cropRect.height : window.innerHeight;
|
|
11
|
-
|
|
12
|
-
return {
|
|
13
|
-
left: targetX - buffer,
|
|
14
|
-
top: targetY - buffer,
|
|
15
|
-
right: targetX + targetWidth + buffer,
|
|
16
|
-
bottom: targetY + targetHeight + buffer
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function isElementInViewport(
|
|
21
|
-
element: HTMLElement,
|
|
22
|
-
bounds: CaptureBounds
|
|
23
|
-
): boolean {
|
|
24
|
-
const rect = element.getBoundingClientRect();
|
|
25
|
-
|
|
26
|
-
// Outside viewport
|
|
27
|
-
if (
|
|
28
|
-
rect.bottom < bounds.top ||
|
|
29
|
-
rect.top > bounds.bottom ||
|
|
30
|
-
rect.right < bounds.left ||
|
|
31
|
-
rect.left > bounds.right
|
|
32
|
-
) {
|
|
33
|
-
return false;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Hidden elements
|
|
37
|
-
if (rect.width === 0 || rect.height === 0) {
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return true;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function collectVisibleElements(bounds: CaptureBounds): Element[] {
|
|
45
|
-
const all = Array.from(document.body.getElementsByTagName('*'));
|
|
46
|
-
|
|
47
|
-
return all.filter(element => {
|
|
48
|
-
// Skip non-HTML elements
|
|
49
|
-
if (!(element instanceof HTMLElement)) return false;
|
|
50
|
-
|
|
51
|
-
// Skip excluded tags
|
|
52
|
-
const tag = element.tagName;
|
|
53
|
-
if (['SCRIPT', 'STYLE', 'LINK', 'IFRAME', 'NOSCRIPT'].includes(tag)) {
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Check viewport visibility (but keep HTML and BODY)
|
|
58
|
-
if (tag === 'HTML' || tag === 'BODY') {
|
|
59
|
-
return true;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return isElementInViewport(element as HTMLElement, bounds);
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function estimateDOMComplexity(element: HTMLElement): {
|
|
67
|
-
nodeCount: number;
|
|
68
|
-
imageCount: number;
|
|
69
|
-
svgCount: number;
|
|
70
|
-
isComplex: boolean;
|
|
71
|
-
} {
|
|
72
|
-
const allNodes = element.getElementsByTagName('*');
|
|
73
|
-
const images = element.getElementsByTagName('img');
|
|
74
|
-
const svgs = element.getElementsByTagName('svg');
|
|
75
|
-
const nodeCount = allNodes.length;
|
|
76
|
-
const imageCount = images.length;
|
|
77
|
-
const svgCount = svgs.length;
|
|
78
|
-
|
|
79
|
-
// Consider DOM complex if it has many nodes, images, or SVGs
|
|
80
|
-
const isComplex = nodeCount > 1000 || imageCount > 50 || svgCount > 10;
|
|
81
|
-
|
|
82
|
-
return { nodeCount, imageCount, svgCount, isComplex };
|
|
83
|
-
}
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import { loadImageWithTimeout } from './performance';
|
|
2
|
-
|
|
3
|
-
export interface ImageRenderResult {
|
|
4
|
-
success: boolean;
|
|
5
|
-
method?: 'canvas' | 'placeholder' | 'skip';
|
|
6
|
-
error?: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Strategy 1: Try canvas rendering with crossOrigin
|
|
11
|
-
*/
|
|
12
|
-
async function tryCanvasRender(
|
|
13
|
-
img: HTMLImageElement,
|
|
14
|
-
ctx: CanvasRenderingContext2D,
|
|
15
|
-
x: number,
|
|
16
|
-
y: number,
|
|
17
|
-
width: number,
|
|
18
|
-
height: number
|
|
19
|
-
): Promise<ImageRenderResult> {
|
|
20
|
-
try {
|
|
21
|
-
// Create a test image with crossOrigin
|
|
22
|
-
const testImg = new Image();
|
|
23
|
-
testImg.crossOrigin = 'anonymous';
|
|
24
|
-
|
|
25
|
-
await loadImageWithTimeout(testImg, img.src, 3000);
|
|
26
|
-
|
|
27
|
-
// Draw to canvas
|
|
28
|
-
ctx.drawImage(testImg, x, y, width, height);
|
|
29
|
-
|
|
30
|
-
return { success: true, method: 'canvas' };
|
|
31
|
-
} catch (error) {
|
|
32
|
-
return {
|
|
33
|
-
success: false,
|
|
34
|
-
error: error instanceof Error ? error.message : 'Canvas render failed'
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Strategy 2: Use placeholder for CORS-blocked images
|
|
41
|
-
*/
|
|
42
|
-
function renderPlaceholder(
|
|
43
|
-
ctx: CanvasRenderingContext2D,
|
|
44
|
-
x: number,
|
|
45
|
-
y: number,
|
|
46
|
-
width: number,
|
|
47
|
-
height: number
|
|
48
|
-
): ImageRenderResult {
|
|
49
|
-
// Draw gray background
|
|
50
|
-
ctx.fillStyle = '#f0f0f0';
|
|
51
|
-
ctx.fillRect(x, y, width, height);
|
|
52
|
-
|
|
53
|
-
// Draw border
|
|
54
|
-
ctx.strokeStyle = '#ccc';
|
|
55
|
-
ctx.lineWidth = 1;
|
|
56
|
-
ctx.strokeRect(x, y, width, height);
|
|
57
|
-
|
|
58
|
-
// Draw "Image" text
|
|
59
|
-
ctx.fillStyle = '#999';
|
|
60
|
-
ctx.font = '12px sans-serif';
|
|
61
|
-
ctx.textAlign = 'center';
|
|
62
|
-
ctx.textBaseline = 'middle';
|
|
63
|
-
ctx.fillText('Image', x + width / 2, y + height / 2);
|
|
64
|
-
|
|
65
|
-
return { success: true, method: 'placeholder' };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Strategy 3: Skip if very small or hidden
|
|
70
|
-
*/
|
|
71
|
-
function shouldSkipImage(width: number, height: number): boolean {
|
|
72
|
-
return width < 5 || height < 5;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Main image rendering function with 3-tier strategy
|
|
77
|
-
*/
|
|
78
|
-
export async function renderImage(
|
|
79
|
-
img: HTMLImageElement,
|
|
80
|
-
ctx: CanvasRenderingContext2D
|
|
81
|
-
): Promise<ImageRenderResult> {
|
|
82
|
-
const rect = img.getBoundingClientRect();
|
|
83
|
-
|
|
84
|
-
// Use viewport coordinates directly
|
|
85
|
-
const x = rect.left;
|
|
86
|
-
const y = rect.top;
|
|
87
|
-
const width = rect.width;
|
|
88
|
-
const height = rect.height;
|
|
89
|
-
|
|
90
|
-
// Strategy 3: Skip very small images
|
|
91
|
-
if (shouldSkipImage(width, height)) {
|
|
92
|
-
return { success: true, method: 'skip' };
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Strategy 1: Try canvas rendering
|
|
96
|
-
const canvasResult = await tryCanvasRender(img, ctx, x, y, width, height);
|
|
97
|
-
if (canvasResult.success) {
|
|
98
|
-
return canvasResult;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Strategy 2: Use placeholder
|
|
102
|
-
return renderPlaceholder(ctx, x, y, width, height);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Render all images in the document
|
|
107
|
-
*/
|
|
108
|
-
export async function renderImages(
|
|
109
|
-
images: HTMLImageElement[],
|
|
110
|
-
ctx: CanvasRenderingContext2D,
|
|
111
|
-
onProgress?: (current: number, total: number) => void
|
|
112
|
-
): Promise<void> {
|
|
113
|
-
for (let i = 0; i < images.length; i++) {
|
|
114
|
-
await renderImage(images[i], ctx);
|
|
115
|
-
|
|
116
|
-
if (onProgress) {
|
|
117
|
-
onProgress(i + 1, images.length);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Check if image is CORS-safe
|
|
124
|
-
*/
|
|
125
|
-
export function isImageCORSSafe(img: HTMLImageElement): boolean {
|
|
126
|
-
// Same origin images are always safe
|
|
127
|
-
try {
|
|
128
|
-
const imgUrl = new URL(img.src, window.location.href);
|
|
129
|
-
return imgUrl.origin === window.location.origin;
|
|
130
|
-
} catch {
|
|
131
|
-
return false;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Get all renderable images from elements
|
|
137
|
-
*/
|
|
138
|
-
export function getImageElements(elements: Element[]): HTMLImageElement[] {
|
|
139
|
-
const images: HTMLImageElement[] = [];
|
|
140
|
-
|
|
141
|
-
for (const element of elements) {
|
|
142
|
-
if (element instanceof HTMLImageElement && element.src) {
|
|
143
|
-
images.push(element);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return images;
|
|
148
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
export function yieldToMainThread(): Promise<void> {
|
|
2
|
-
return new Promise(resolve => {
|
|
3
|
-
// Use Scheduler API if available (Chrome 94+)
|
|
4
|
-
if ('scheduler' in window && 'yield' in (window as { scheduler?: { yield?: () => Promise<void> } }).scheduler!) {
|
|
5
|
-
(window as { scheduler: { yield: () => Promise<void> } }).scheduler.yield().then(resolve);
|
|
6
|
-
} else {
|
|
7
|
-
// Fallback: setTimeout
|
|
8
|
-
setTimeout(resolve, 0);
|
|
9
|
-
}
|
|
10
|
-
});
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export async function processInChunks<T>(
|
|
14
|
-
items: T[],
|
|
15
|
-
chunkSize: number,
|
|
16
|
-
processor: (chunk: T[]) => Promise<void>,
|
|
17
|
-
onProgress?: (progress: number) => void
|
|
18
|
-
): Promise<void> {
|
|
19
|
-
for (let i = 0; i < items.length; i += chunkSize) {
|
|
20
|
-
const chunk = items.slice(i, i + chunkSize);
|
|
21
|
-
|
|
22
|
-
// Process chunk
|
|
23
|
-
await processor(chunk);
|
|
24
|
-
|
|
25
|
-
// Report progress
|
|
26
|
-
if (onProgress) {
|
|
27
|
-
onProgress(Math.round(((i + chunk.length) / items.length) * 100));
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Yield to main thread
|
|
31
|
-
await yieldToMainThread();
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function loadImageWithTimeout(
|
|
36
|
-
img: HTMLImageElement,
|
|
37
|
-
url: string,
|
|
38
|
-
timeout: number
|
|
39
|
-
): Promise<void> {
|
|
40
|
-
return new Promise((resolve, reject) => {
|
|
41
|
-
const timer = setTimeout(() => {
|
|
42
|
-
reject(new Error('Image load timeout'));
|
|
43
|
-
}, timeout);
|
|
44
|
-
|
|
45
|
-
img.onload = () => {
|
|
46
|
-
clearTimeout(timer);
|
|
47
|
-
resolve();
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
img.onerror = () => {
|
|
51
|
-
clearTimeout(timer);
|
|
52
|
-
reject(new Error('Image load error'));
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
img.src = url;
|
|
56
|
-
});
|
|
57
|
-
}
|