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,212 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Text Content Renderer - Layer 4
|
|
3
|
-
* Renders HTML text elements to canvas using SVG foreignObject technique
|
|
4
|
-
* This preserves exact browser rendering including fonts, wrapping, and nested styles
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export interface TextRenderResult {
|
|
8
|
-
success: boolean;
|
|
9
|
-
error?: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Render HTML element as image using SVG foreignObject
|
|
14
|
-
* This technique captures the exact browser rendering of text with all styles
|
|
15
|
-
*/
|
|
16
|
-
export async function renderTextElement(
|
|
17
|
-
element: HTMLElement,
|
|
18
|
-
ctx: CanvasRenderingContext2D
|
|
19
|
-
): Promise<TextRenderResult> {
|
|
20
|
-
try {
|
|
21
|
-
const rect = element.getBoundingClientRect();
|
|
22
|
-
|
|
23
|
-
// Use viewport coordinates directly (canvas is already translated)
|
|
24
|
-
const x = rect.left;
|
|
25
|
-
const y = rect.top;
|
|
26
|
-
const width = rect.width;
|
|
27
|
-
const height = rect.height;
|
|
28
|
-
|
|
29
|
-
// Skip elements with no dimensions
|
|
30
|
-
if (width < 1 || height < 1) {
|
|
31
|
-
return { success: true };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Get computed styles to preserve appearance
|
|
35
|
-
const styles = window.getComputedStyle(element);
|
|
36
|
-
|
|
37
|
-
// Clone element to avoid modifying original
|
|
38
|
-
const clonedElement = element.cloneNode(true) as HTMLElement;
|
|
39
|
-
|
|
40
|
-
// Get all relevant styles
|
|
41
|
-
const relevantStyles = [
|
|
42
|
-
'color',
|
|
43
|
-
'font-family',
|
|
44
|
-
'font-size',
|
|
45
|
-
'font-weight',
|
|
46
|
-
'font-style',
|
|
47
|
-
'line-height',
|
|
48
|
-
'text-align',
|
|
49
|
-
'text-decoration',
|
|
50
|
-
'text-transform',
|
|
51
|
-
'letter-spacing',
|
|
52
|
-
'word-spacing',
|
|
53
|
-
'white-space',
|
|
54
|
-
'padding',
|
|
55
|
-
'margin',
|
|
56
|
-
'display',
|
|
57
|
-
'flex-direction',
|
|
58
|
-
'align-items',
|
|
59
|
-
'justify-content'
|
|
60
|
-
];
|
|
61
|
-
|
|
62
|
-
// Build inline styles from computed styles
|
|
63
|
-
let inlineStyle = '';
|
|
64
|
-
relevantStyles.forEach(prop => {
|
|
65
|
-
const value = styles.getPropertyValue(prop);
|
|
66
|
-
if (value) {
|
|
67
|
-
inlineStyle += `${prop}: ${value}; `;
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
// Apply styles to cloned element
|
|
72
|
-
clonedElement.setAttribute('style', inlineStyle);
|
|
73
|
-
|
|
74
|
-
// Wrap in foreignObject SVG
|
|
75
|
-
const foreignObject = `
|
|
76
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
|
|
77
|
-
<foreignObject width="100%" height="100%">
|
|
78
|
-
<div xmlns="http://www.w3.org/1999/xhtml" style="width: ${width}px; height: ${height}px; overflow: hidden;">
|
|
79
|
-
${clonedElement.outerHTML}
|
|
80
|
-
</div>
|
|
81
|
-
</foreignObject>
|
|
82
|
-
</svg>
|
|
83
|
-
`;
|
|
84
|
-
|
|
85
|
-
// Create blob URL
|
|
86
|
-
const blob = new Blob([foreignObject], { type: 'image/svg+xml;charset=utf-8' });
|
|
87
|
-
const url = URL.createObjectURL(blob);
|
|
88
|
-
|
|
89
|
-
try {
|
|
90
|
-
// Load as image with timeout
|
|
91
|
-
const img = new Image();
|
|
92
|
-
await new Promise<void>((resolve, reject) => {
|
|
93
|
-
const timeout = setTimeout(() => {
|
|
94
|
-
reject(new Error('Text element render timeout'));
|
|
95
|
-
}, 1000); // 1 second timeout
|
|
96
|
-
|
|
97
|
-
img.onload = () => {
|
|
98
|
-
clearTimeout(timeout);
|
|
99
|
-
resolve();
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
img.onerror = () => {
|
|
103
|
-
clearTimeout(timeout);
|
|
104
|
-
reject(new Error('Text element image load failed'));
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
img.src = url;
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
// Draw to canvas
|
|
111
|
-
ctx.drawImage(img, x, y, width, height);
|
|
112
|
-
|
|
113
|
-
return { success: true };
|
|
114
|
-
} finally {
|
|
115
|
-
// Clean up blob URL
|
|
116
|
-
URL.revokeObjectURL(url);
|
|
117
|
-
}
|
|
118
|
-
} catch (error) {
|
|
119
|
-
console.warn('[TextRenderer] Failed to render text element:', error);
|
|
120
|
-
return {
|
|
121
|
-
success: false,
|
|
122
|
-
error: error instanceof Error ? error.message : 'Text render failed'
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Check if element contains renderable text content
|
|
129
|
-
*/
|
|
130
|
-
function hasTextContent(element: HTMLElement): boolean {
|
|
131
|
-
// Skip non-visible elements
|
|
132
|
-
const styles = window.getComputedStyle(element);
|
|
133
|
-
if (styles.display === 'none' || styles.visibility === 'hidden') {
|
|
134
|
-
return false;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Check if element or its children have text
|
|
138
|
-
const text = element.textContent?.trim();
|
|
139
|
-
if (!text || text.length === 0) {
|
|
140
|
-
return false;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return true;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Get text elements suitable for rendering
|
|
148
|
-
* Selects leaf elements that contain actual text content
|
|
149
|
-
*/
|
|
150
|
-
export function getTextElements(elements: Element[]): HTMLElement[] {
|
|
151
|
-
const textElements: HTMLElement[] = [];
|
|
152
|
-
|
|
153
|
-
for (const element of elements) {
|
|
154
|
-
if (!(element instanceof HTMLElement)) continue;
|
|
155
|
-
|
|
156
|
-
const tag = element.tagName;
|
|
157
|
-
|
|
158
|
-
// Skip elements that are rendered by other layers
|
|
159
|
-
if (['IMG', 'SVG', 'CANVAS', 'VIDEO', 'IFRAME', 'SCRIPT', 'STYLE', 'NOSCRIPT'].includes(tag)) {
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Focus on text-containing elements
|
|
164
|
-
// Prioritize leaf elements (P, SPAN, H1-H6, A, etc.)
|
|
165
|
-
const isTextElement = [
|
|
166
|
-
'P', 'SPAN', 'DIV', 'A', 'BUTTON', 'LABEL', 'LI',
|
|
167
|
-
'H1', 'H2', 'H3', 'H4', 'H5', 'H6',
|
|
168
|
-
'TD', 'TH', 'STRONG', 'EM', 'B', 'I', 'U',
|
|
169
|
-
'CODE', 'PRE', 'BLOCKQUOTE'
|
|
170
|
-
].includes(tag);
|
|
171
|
-
|
|
172
|
-
if (isTextElement && hasTextContent(element)) {
|
|
173
|
-
// Check if this is a leaf element or has only text children
|
|
174
|
-
const hasBlockChildren = Array.from(element.children).some(child => {
|
|
175
|
-
const childTag = child.tagName;
|
|
176
|
-
return ['DIV', 'P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'UL', 'OL', 'TABLE'].includes(childTag);
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
// Only add if it's a leaf element (no block children)
|
|
180
|
-
if (!hasBlockChildren) {
|
|
181
|
-
textElements.push(element);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return textElements;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Render all text elements to canvas
|
|
191
|
-
*/
|
|
192
|
-
export async function renderTextContent(
|
|
193
|
-
elements: Element[],
|
|
194
|
-
ctx: CanvasRenderingContext2D,
|
|
195
|
-
onProgress?: (current: number, total: number) => void
|
|
196
|
-
): Promise<void> {
|
|
197
|
-
const textElements = getTextElements(elements);
|
|
198
|
-
|
|
199
|
-
console.log('[TextRenderer] Rendering', textElements.length, 'text elements');
|
|
200
|
-
|
|
201
|
-
for (let i = 0; i < textElements.length; i++) {
|
|
202
|
-
const result = await renderTextElement(textElements[i], ctx);
|
|
203
|
-
|
|
204
|
-
if (!result.success) {
|
|
205
|
-
console.warn('[TextRenderer] Failed to render element:', textElements[i], result.error);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
if (onProgress) {
|
|
209
|
-
onProgress(i + 1, textElements.length);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
export interface Rect {
|
|
2
|
-
x: number;
|
|
3
|
-
y: number;
|
|
4
|
-
width: number;
|
|
5
|
-
height: number;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface DeviceCapabilities {
|
|
9
|
-
maxCanvasSize: number;
|
|
10
|
-
recommendedScale: number;
|
|
11
|
-
recommendedQuality: number;
|
|
12
|
-
chunkSize: number;
|
|
13
|
-
yieldInterval: number;
|
|
14
|
-
maxTimeout: number;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface CaptureOptions {
|
|
18
|
-
quality: number;
|
|
19
|
-
scale: number;
|
|
20
|
-
includeMathJax: boolean;
|
|
21
|
-
timeout: number;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface CaptureProgress {
|
|
25
|
-
phase: 'analyzing' | 'rendering' | 'exporting';
|
|
26
|
-
progress: number; // 0-100
|
|
27
|
-
message: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface CleanupTracker {
|
|
31
|
-
objectUrls: string[];
|
|
32
|
-
tempCanvases: HTMLCanvasElement[];
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface CaptureBounds {
|
|
36
|
-
left: number;
|
|
37
|
-
top: number;
|
|
38
|
-
right: number;
|
|
39
|
-
bottom: number;
|
|
40
|
-
}
|
package/src/utils/screenshot.ts
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import type { Rect, CaptureProgress } from './screenshot/types';
|
|
2
|
-
import { captureWithCanvas } from './screenshot/canvas-renderer';
|
|
3
|
-
import { domToPng } from 'modern-screenshot';
|
|
4
|
-
|
|
5
|
-
// Re-export Rect type for backwards compatibility
|
|
6
|
-
export type { Rect };
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Capture screenshot with 3-tier fallback strategy:
|
|
10
|
-
* 1. Canvas-based capture (primary)
|
|
11
|
-
* 2. modern-screenshot library (fallback)
|
|
12
|
-
* 3. Return null (final fallback)
|
|
13
|
-
*/
|
|
14
|
-
export const captureScreenshot = async (
|
|
15
|
-
cropRect?: Rect,
|
|
16
|
-
onProgress?: (progress: CaptureProgress) => void
|
|
17
|
-
): Promise<string | null> => {
|
|
18
|
-
console.log('[Screenshot] Starting capture with 3-tier fallback strategy');
|
|
19
|
-
|
|
20
|
-
try {
|
|
21
|
-
// Tier 1: Try canvas-based capture
|
|
22
|
-
console.log('[Screenshot] Tier 1: Attempting canvas-based capture...');
|
|
23
|
-
const canvasResult = await captureWithCanvas(cropRect, {}, onProgress);
|
|
24
|
-
|
|
25
|
-
if (canvasResult) {
|
|
26
|
-
console.log('[Screenshot] Tier 1 successful - canvas-based capture');
|
|
27
|
-
return canvasResult;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
console.warn('[Screenshot] Tier 1 failed, falling back to Tier 2');
|
|
31
|
-
} catch (error) {
|
|
32
|
-
console.warn('[Screenshot] Tier 1 error:', error);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
try {
|
|
36
|
-
// Tier 2: Fallback to modern-screenshot
|
|
37
|
-
console.log('[Screenshot] Tier 2: Attempting modern-screenshot...');
|
|
38
|
-
|
|
39
|
-
// Use Visual Viewport API for accurate mobile dimensions
|
|
40
|
-
const getViewportWidth = () => {
|
|
41
|
-
if (window.visualViewport) {
|
|
42
|
-
return window.visualViewport.width;
|
|
43
|
-
}
|
|
44
|
-
return document.documentElement.clientWidth || window.innerWidth;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const getViewportHeight = () => {
|
|
48
|
-
if (window.visualViewport) {
|
|
49
|
-
return window.visualViewport.height;
|
|
50
|
-
}
|
|
51
|
-
return document.documentElement.clientHeight || window.innerHeight;
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
const element = document.documentElement;
|
|
55
|
-
const targetX = cropRect ? cropRect.x : 0;
|
|
56
|
-
const targetY = cropRect ? cropRect.y : 0;
|
|
57
|
-
const targetWidth = cropRect ? cropRect.width : getViewportWidth();
|
|
58
|
-
const targetHeight = cropRect ? cropRect.height : getViewportHeight();
|
|
59
|
-
|
|
60
|
-
const scrollX = window.scrollX;
|
|
61
|
-
const scrollY = window.scrollY;
|
|
62
|
-
const offsetX = scrollX + targetX;
|
|
63
|
-
const offsetY = scrollY + targetY;
|
|
64
|
-
|
|
65
|
-
const dataUrl = await domToPng(element, {
|
|
66
|
-
quality: 0.9,
|
|
67
|
-
width: targetWidth,
|
|
68
|
-
height: targetHeight,
|
|
69
|
-
style: {
|
|
70
|
-
transform: `translate(${-offsetX}px, ${-offsetY}px)`
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
console.log('[Screenshot] Tier 2 successful - modern-screenshot');
|
|
75
|
-
return dataUrl;
|
|
76
|
-
} catch (error) {
|
|
77
|
-
console.error('[Screenshot] Tier 2 failed:', error);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Tier 3: All methods failed
|
|
81
|
-
console.error('[Screenshot] All capture methods failed');
|
|
82
|
-
return null;
|
|
83
|
-
};
|