hocmai-feedback-widget 0.2.1 → 0.2.3
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 +78 -113
- 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 +13 -1
- package/dist/context/FeedbackContext.d.ts +14 -2
- package/dist/hocmai-feedback-widget.mjs +7550 -0
- package/dist/hocmai-feedback-widget.mjs.map +1 -0
- package/dist/index.d.ts +4 -2
- package/package.json +39 -29
- package/src/App.tsx +147 -58
- package/src/capture/device-detection.ts +52 -0
- package/src/capture/fonts.ts +34 -0
- package/src/capture/index.ts +361 -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 +105 -93
- package/src/context/FeedbackContext.tsx +49 -9
- package/src/index.ts +9 -3
- package/src/main.tsx +7 -9
- package/src/utils/device.ts +14 -14
- package/dist/support-feedback-lib.es.js +0 -3946
- package/dist/support-feedback-lib.umd.js +0 -84
- package/dist/test-mobile-screenshot.html +0 -211
- package/dist/test-simple-capture.html +0 -107
- 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 -16
- 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 -343
- package/src/utils/screenshot/cleanup.ts +0 -32
- package/src/utils/screenshot/device-detection.ts +0 -90
- package/src/utils/screenshot/element-collector.ts +0 -134
- 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 -149
- package/src/utils/screenshot/text-renderer.ts +0 -212
- package/src/utils/screenshot/types.ts +0 -40
- package/src/utils/screenshot.ts +0 -83
|
@@ -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,90 +0,0 @@
|
|
|
1
|
-
import type { DeviceCapabilities } from './types';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Detect if browser is Safari (including iOS Safari)
|
|
5
|
-
*/
|
|
6
|
-
export function isSafari(): boolean {
|
|
7
|
-
const userAgent = navigator.userAgent.toLowerCase();
|
|
8
|
-
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent) ||
|
|
9
|
-
/iphone|ipad|ipod/.test(userAgent);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Detect if browser is iOS Safari specifically
|
|
14
|
-
* Must exclude Chrome/Chromium on iOS (Chrome DevTools emulation, Chrome iOS app)
|
|
15
|
-
*/
|
|
16
|
-
export function isIOSSafari(): boolean {
|
|
17
|
-
const userAgent = navigator.userAgent.toLowerCase();
|
|
18
|
-
console.log('[DeviceDetection] User Agent:', userAgent);
|
|
19
|
-
|
|
20
|
-
// Check for iOS device
|
|
21
|
-
const isIOS = /iphone|ipad|ipod/.test(userAgent);
|
|
22
|
-
console.log('[DeviceDetection] isIOS:', isIOS);
|
|
23
|
-
|
|
24
|
-
// Check it's Safari (has 'safari' but NOT 'chrome' or 'crios' or 'crmo')
|
|
25
|
-
const isSafariBrowser = /safari/.test(userAgent) &&
|
|
26
|
-
!/chrome|crios|crmo/.test(userAgent);
|
|
27
|
-
console.log('[DeviceDetection] isSafariBrowser:', isSafariBrowser);
|
|
28
|
-
console.log('[DeviceDetection] Final isIOSSafari:', isIOS && isSafariBrowser);
|
|
29
|
-
|
|
30
|
-
return isIOS && isSafariBrowser;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Check if browser supports SVG foreignObject reliably
|
|
35
|
-
* Safari iOS has known issues with foreignObject causing canvas tainting
|
|
36
|
-
*/
|
|
37
|
-
export function supportsForeignObject(): boolean {
|
|
38
|
-
// Safari iOS < 13 has poor foreignObject support
|
|
39
|
-
// Even newer versions can cause canvas tainting issues
|
|
40
|
-
if (isIOSSafari()) {
|
|
41
|
-
console.warn('[DeviceDetection] Safari iOS detected - foreignObject may cause issues');
|
|
42
|
-
return false;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Desktop Safari also has some limitations
|
|
46
|
-
if (isSafari() && !isIOSSafari()) {
|
|
47
|
-
console.warn('[DeviceDetection] Desktop Safari detected - foreignObject may have limitations');
|
|
48
|
-
// Desktop Safari works better, so we allow it
|
|
49
|
-
return true;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return true;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function getDeviceCapabilities(): DeviceCapabilities {
|
|
56
|
-
const userAgent = navigator.userAgent.toLowerCase();
|
|
57
|
-
const isMobile = /iphone|ipod|android.*mobile/.test(userAgent);
|
|
58
|
-
const isTablet = /ipad|android(?!.*mobile)/.test(userAgent);
|
|
59
|
-
const deviceMemory = (navigator as { deviceMemory?: number }).deviceMemory || 4;
|
|
60
|
-
const isLowMemory = deviceMemory < 4;
|
|
61
|
-
|
|
62
|
-
if (isMobile) {
|
|
63
|
-
return {
|
|
64
|
-
maxCanvasSize: 4096,
|
|
65
|
-
recommendedScale: isLowMemory ? 0.8 : 1,
|
|
66
|
-
recommendedQuality: isLowMemory ? 0.7 : 0.8,
|
|
67
|
-
chunkSize: 30,
|
|
68
|
-
yieldInterval: 16, // ~60fps
|
|
69
|
-
maxTimeout: 5000
|
|
70
|
-
};
|
|
71
|
-
} else if (isTablet) {
|
|
72
|
-
return {
|
|
73
|
-
maxCanvasSize: 8192,
|
|
74
|
-
recommendedScale: 1,
|
|
75
|
-
recommendedQuality: 0.85,
|
|
76
|
-
chunkSize: 50,
|
|
77
|
-
yieldInterval: 16,
|
|
78
|
-
maxTimeout: 4000
|
|
79
|
-
};
|
|
80
|
-
} else {
|
|
81
|
-
return {
|
|
82
|
-
maxCanvasSize: 16384,
|
|
83
|
-
recommendedScale: 1,
|
|
84
|
-
recommendedQuality: 0.9,
|
|
85
|
-
chunkSize: 100,
|
|
86
|
-
yieldInterval: 8,
|
|
87
|
-
maxTimeout: 3000
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
}
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import type { Rect, CaptureBounds } from './types';
|
|
2
|
-
|
|
3
|
-
export function calculateCaptureBounds(
|
|
4
|
-
cropRect: Rect | undefined,
|
|
5
|
-
buffer: number,
|
|
6
|
-
viewportWidth?: number,
|
|
7
|
-
viewportHeight?: number
|
|
8
|
-
): CaptureBounds {
|
|
9
|
-
// Use provided viewport dimensions or calculate them
|
|
10
|
-
const getViewportWidth = () => {
|
|
11
|
-
if (viewportWidth !== undefined) return viewportWidth;
|
|
12
|
-
if (window.visualViewport) return window.visualViewport.width;
|
|
13
|
-
return document.documentElement.clientWidth || window.innerWidth;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
const getViewportHeight = () => {
|
|
17
|
-
if (viewportHeight !== undefined) return viewportHeight;
|
|
18
|
-
if (window.visualViewport) return window.visualViewport.height;
|
|
19
|
-
return document.documentElement.clientHeight || window.innerHeight;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const targetX = cropRect ? cropRect.x : 0;
|
|
23
|
-
const targetY = cropRect ? cropRect.y : 0;
|
|
24
|
-
const targetWidth = cropRect ? cropRect.width : getViewportWidth();
|
|
25
|
-
const targetHeight = cropRect ? cropRect.height : getViewportHeight();
|
|
26
|
-
|
|
27
|
-
const bounds = {
|
|
28
|
-
left: targetX - buffer,
|
|
29
|
-
top: targetY - buffer,
|
|
30
|
-
right: targetX + targetWidth + buffer,
|
|
31
|
-
bottom: targetY + targetHeight + buffer
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
console.log('[ElementCollector] calculateCaptureBounds:', {
|
|
35
|
-
viewportWidth: viewportWidth || 'auto',
|
|
36
|
-
viewportHeight: viewportHeight || 'auto',
|
|
37
|
-
targetWidth,
|
|
38
|
-
targetHeight,
|
|
39
|
-
bounds,
|
|
40
|
-
scroll: { x: window.scrollX, y: window.scrollY }
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
return bounds;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function isElementInViewport(
|
|
47
|
-
element: HTMLElement,
|
|
48
|
-
bounds: CaptureBounds
|
|
49
|
-
): boolean {
|
|
50
|
-
const rect = element.getBoundingClientRect();
|
|
51
|
-
|
|
52
|
-
// Convert bounds from document coordinates to viewport coordinates
|
|
53
|
-
// getBoundingClientRect() returns viewport-relative coordinates
|
|
54
|
-
// bounds are in document coordinates, so subtract scroll offset
|
|
55
|
-
const scrollX = window.scrollX || window.pageXOffset;
|
|
56
|
-
const scrollY = window.scrollY || window.pageYOffset;
|
|
57
|
-
|
|
58
|
-
const viewportBounds = {
|
|
59
|
-
left: bounds.left - scrollX,
|
|
60
|
-
top: bounds.top - scrollY,
|
|
61
|
-
right: bounds.right - scrollX,
|
|
62
|
-
bottom: bounds.bottom - scrollY
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
// Outside viewport
|
|
66
|
-
if (
|
|
67
|
-
rect.bottom < viewportBounds.top ||
|
|
68
|
-
rect.top > viewportBounds.bottom ||
|
|
69
|
-
rect.right < viewportBounds.left ||
|
|
70
|
-
rect.left > viewportBounds.right
|
|
71
|
-
) {
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Hidden elements
|
|
76
|
-
if (rect.width === 0 || rect.height === 0) {
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return true;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function collectVisibleElements(bounds: CaptureBounds): Element[] {
|
|
84
|
-
const all = Array.from(document.body.getElementsByTagName('*'));
|
|
85
|
-
|
|
86
|
-
const filtered = all.filter(element => {
|
|
87
|
-
// Skip non-HTML elements
|
|
88
|
-
if (!(element instanceof HTMLElement)) return false;
|
|
89
|
-
|
|
90
|
-
// Skip excluded tags
|
|
91
|
-
const tag = element.tagName;
|
|
92
|
-
if (['SCRIPT', 'STYLE', 'LINK', 'IFRAME', 'NOSCRIPT'].includes(tag)) {
|
|
93
|
-
return false;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Check viewport visibility (but keep HTML and BODY)
|
|
97
|
-
if (tag === 'HTML' || tag === 'BODY') {
|
|
98
|
-
return true;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return isElementInViewport(element as HTMLElement, bounds);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
console.log('[ElementCollector] collectVisibleElements:', {
|
|
105
|
-
total: all.length,
|
|
106
|
-
filtered: filtered.length,
|
|
107
|
-
bounds,
|
|
108
|
-
sampleElements: filtered.slice(0, 5).map(el => ({
|
|
109
|
-
tag: el.tagName,
|
|
110
|
-
rect: el.getBoundingClientRect()
|
|
111
|
-
}))
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
return filtered;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export function estimateDOMComplexity(element: HTMLElement): {
|
|
118
|
-
nodeCount: number;
|
|
119
|
-
imageCount: number;
|
|
120
|
-
svgCount: number;
|
|
121
|
-
isComplex: boolean;
|
|
122
|
-
} {
|
|
123
|
-
const allNodes = element.getElementsByTagName('*');
|
|
124
|
-
const images = element.getElementsByTagName('img');
|
|
125
|
-
const svgs = element.getElementsByTagName('svg');
|
|
126
|
-
const nodeCount = allNodes.length;
|
|
127
|
-
const imageCount = images.length;
|
|
128
|
-
const svgCount = svgs.length;
|
|
129
|
-
|
|
130
|
-
// Consider DOM complex if it has many nodes, images, or SVGs
|
|
131
|
-
const isComplex = nodeCount > 1000 || imageCount > 50 || svgCount > 10;
|
|
132
|
-
|
|
133
|
-
return { nodeCount, imageCount, svgCount, isComplex };
|
|
134
|
-
}
|
|
@@ -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
|
-
}
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
export interface SVGRenderResult {
|
|
2
|
-
success: boolean;
|
|
3
|
-
error?: string;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Serialize SVG element to string
|
|
8
|
-
*/
|
|
9
|
-
function serializeSVG(svg: SVGElement): string {
|
|
10
|
-
const serializer = new XMLSerializer();
|
|
11
|
-
let svgString = serializer.serializeToString(svg);
|
|
12
|
-
|
|
13
|
-
// Ensure xmlns is present
|
|
14
|
-
if (!svgString.includes('xmlns=')) {
|
|
15
|
-
svgString = svgString.replace(
|
|
16
|
-
'<svg',
|
|
17
|
-
'<svg xmlns="http://www.w3.org/2000/svg"'
|
|
18
|
-
);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return svgString;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Check if SVG contains external resources that might cause CORS issues
|
|
26
|
-
*/
|
|
27
|
-
function hasExternalResources(svgString: string): boolean {
|
|
28
|
-
// Check for external image references
|
|
29
|
-
if (svgString.includes('xlink:href="http') || svgString.includes('href="http')) {
|
|
30
|
-
return true;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Check for external font references
|
|
34
|
-
if (svgString.includes('@font-face') && svgString.includes('url(http')) {
|
|
35
|
-
return true;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Render SVG element to canvas
|
|
43
|
-
*/
|
|
44
|
-
export async function renderSVG(
|
|
45
|
-
svg: SVGElement,
|
|
46
|
-
ctx: CanvasRenderingContext2D
|
|
47
|
-
): Promise<SVGRenderResult> {
|
|
48
|
-
try {
|
|
49
|
-
const rect = svg.getBoundingClientRect();
|
|
50
|
-
|
|
51
|
-
// Use viewport coordinates directly
|
|
52
|
-
const x = rect.left;
|
|
53
|
-
const y = rect.top;
|
|
54
|
-
const width = rect.width;
|
|
55
|
-
const height = rect.height;
|
|
56
|
-
|
|
57
|
-
// Skip very small SVGs
|
|
58
|
-
if (width < 1 || height < 1) {
|
|
59
|
-
return { success: true };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Serialize SVG to string
|
|
63
|
-
const svgString = serializeSVG(svg);
|
|
64
|
-
|
|
65
|
-
// Check for external resources that might cause CORS issues
|
|
66
|
-
if (hasExternalResources(svgString)) {
|
|
67
|
-
console.warn('[SVG] Skipping SVG with external resources to prevent canvas tainting');
|
|
68
|
-
return { success: true }; // Skip but don't fail
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Create blob URL
|
|
72
|
-
const blob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
|
|
73
|
-
const url = URL.createObjectURL(blob);
|
|
74
|
-
|
|
75
|
-
try {
|
|
76
|
-
// Load as image
|
|
77
|
-
const img = new Image();
|
|
78
|
-
await new Promise<void>((resolve, reject) => {
|
|
79
|
-
img.onload = () => resolve();
|
|
80
|
-
img.onerror = () => reject(new Error('SVG image load failed'));
|
|
81
|
-
img.src = url;
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
// Draw to canvas
|
|
85
|
-
ctx.drawImage(img, x, y, width, height);
|
|
86
|
-
|
|
87
|
-
return { success: true };
|
|
88
|
-
} finally {
|
|
89
|
-
// Clean up blob URL
|
|
90
|
-
URL.revokeObjectURL(url);
|
|
91
|
-
}
|
|
92
|
-
} catch (error) {
|
|
93
|
-
return {
|
|
94
|
-
success: false,
|
|
95
|
-
error: error instanceof Error ? error.message : 'SVG render failed'
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Render all SVG elements
|
|
102
|
-
*/
|
|
103
|
-
export async function renderSVGs(
|
|
104
|
-
svgs: SVGElement[],
|
|
105
|
-
ctx: CanvasRenderingContext2D,
|
|
106
|
-
onProgress?: (current: number, total: number) => void
|
|
107
|
-
): Promise<void> {
|
|
108
|
-
for (let i = 0; i < svgs.length; i++) {
|
|
109
|
-
await renderSVG(svgs[i], ctx);
|
|
110
|
-
|
|
111
|
-
if (onProgress) {
|
|
112
|
-
onProgress(i + 1, svgs.length);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Check if element is MathJax rendered
|
|
119
|
-
*/
|
|
120
|
-
export function isMathJaxElement(element: Element): boolean {
|
|
121
|
-
return (
|
|
122
|
-
element.classList.contains('MathJax') ||
|
|
123
|
-
element.classList.contains('MathJax_Display') ||
|
|
124
|
-
element.classList.contains('MJX-TeXAtom-ORD') ||
|
|
125
|
-
element.hasAttribute('data-mathml')
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Get all SVG elements from the document
|
|
131
|
-
*/
|
|
132
|
-
export function getSVGElements(elements: Element[]): SVGElement[] {
|
|
133
|
-
const svgs: SVGElement[] = [];
|
|
134
|
-
|
|
135
|
-
for (const element of elements) {
|
|
136
|
-
if (element instanceof SVGElement) {
|
|
137
|
-
svgs.push(element);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return svgs;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Get all MathJax elements from the document
|
|
146
|
-
*/
|
|
147
|
-
export function getMathJaxElements(elements: Element[]): Element[] {
|
|
148
|
-
return elements.filter(element => isMathJaxElement(element));
|
|
149
|
-
}
|